assign-gingerly 0.0.47 → 0.0.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/assignFeatures.js CHANGED
@@ -386,47 +386,92 @@ export function assignFeatures(ctr, features, featuresRegistry) {
386
386
  if (!supportedFeatures) {
387
387
  throw new Error(`assignFeatures: ${ctr.name || 'constructor'} does not define static supportedFeatures`);
388
388
  }
389
- const onAssignedPromises = [];
389
+ let hasAsync = false;
390
+ async function processFeatures() {
391
+ for (const key of Object.keys(features)) {
392
+ // 1. Confirm the key is opted-in via supportedFeatures
393
+ if (!(key in supportedFeatures)) {
394
+ throw new Error(`assignFeatures: "${key}" is not declared in ${ctr.name || 'constructor'}.supportedFeatures`);
395
+ }
396
+ // 2. Check that the prototype doesn't already have this property defined
397
+ const existingDescriptor = Object.getOwnPropertyDescriptor(ctr.prototype, key);
398
+ if (existingDescriptor) {
399
+ throw new Error(`assignFeatures: "${key}" already exists on ${ctr.name || 'constructor'}.prototype`);
400
+ }
401
+ // 3. Check that this key hasn't already been registered for this constructor
402
+ if (featuresRegistry.hasKey(ctr, key)) {
403
+ throw new Error(`assignFeatures: "${key}" has already been assigned for ${ctr.name || 'constructor'}`);
404
+ }
405
+ // 4. Register the injection
406
+ featuresRegistry.set(ctr, key, features[key]);
407
+ // 5. Install the lazy getter on the prototype
408
+ installFeatureGetter(ctr, key, featuresRegistry);
409
+ // 6. Install callback forwarding if configured (merge author + consumer)
410
+ const featureConfig = features[key];
411
+ const optIn = supportedFeatures[key];
412
+ const authorCallbacks = optIn.callbackForwarding || [];
413
+ const consumerCallbacks = featureConfig.callbackForwarding || [];
414
+ // Union of both (author defaults + consumer additions)
415
+ const allCallbacks = [...new Set([...authorCallbacks, ...consumerCallbacks])];
416
+ if (allCallbacks.length > 0) {
417
+ installCallbackForwarding(ctr, key, allCallbacks);
418
+ }
419
+ // 7. Call static onAssigned if the spawn class defines it (sequentially awaited)
420
+ const SpawnClass = featureConfig.spawn;
421
+ if (SpawnClass && !isAsyncSpawn(SpawnClass) &&
422
+ Object.hasOwn(SpawnClass, 'onAssigned') &&
423
+ typeof SpawnClass.onAssigned === 'function') {
424
+ const result = SpawnClass.onAssigned(ctr, featureConfig, key);
425
+ if (result && typeof result.then === 'function') {
426
+ hasAsync = true;
427
+ await result;
428
+ }
429
+ }
430
+ }
431
+ // 8. Install whenFeatureReady method if featuresConfig.lifecycleKeys is configured
432
+ const featuresConfig = ctr.featuresConfig;
433
+ if (featuresConfig?.lifecycleKeys) {
434
+ const methodName = resolveWhenFeatureReadyName(featuresConfig.lifecycleKeys);
435
+ if (methodName) {
436
+ installWhenFeatureReadyMethod(ctr, methodName);
437
+ }
438
+ }
439
+ }
440
+ // Check if any feature has an async onAssigned (pre-scan)
441
+ for (const key of Object.keys(features)) {
442
+ const featureConfig = features[key];
443
+ const SpawnClass = featureConfig.spawn;
444
+ if (SpawnClass && !isAsyncSpawn(SpawnClass) &&
445
+ Object.hasOwn(SpawnClass, 'onAssigned') &&
446
+ typeof SpawnClass.onAssigned === 'function') {
447
+ // We can't know if it's async without calling it, so always use the async path
448
+ // if any onAssigned exists
449
+ return processFeatures();
450
+ }
451
+ }
452
+ // No onAssigned hooks — run synchronously (inline the logic to avoid the async wrapper)
390
453
  for (const key of Object.keys(features)) {
391
- // 1. Confirm the key is opted-in via supportedFeatures
392
454
  if (!(key in supportedFeatures)) {
393
455
  throw new Error(`assignFeatures: "${key}" is not declared in ${ctr.name || 'constructor'}.supportedFeatures`);
394
456
  }
395
- // 2. Check that the prototype doesn't already have this property defined
396
457
  const existingDescriptor = Object.getOwnPropertyDescriptor(ctr.prototype, key);
397
458
  if (existingDescriptor) {
398
459
  throw new Error(`assignFeatures: "${key}" already exists on ${ctr.name || 'constructor'}.prototype`);
399
460
  }
400
- // 3. Check that this key hasn't already been registered for this constructor
401
461
  if (featuresRegistry.hasKey(ctr, key)) {
402
462
  throw new Error(`assignFeatures: "${key}" has already been assigned for ${ctr.name || 'constructor'}`);
403
463
  }
404
- // 4. Register the injection
405
464
  featuresRegistry.set(ctr, key, features[key]);
406
- // 5. Install the lazy getter on the prototype
407
465
  installFeatureGetter(ctr, key, featuresRegistry);
408
- // 6. Install callback forwarding if configured (merge author + consumer)
409
466
  const featureConfig = features[key];
410
467
  const optIn = supportedFeatures[key];
411
468
  const authorCallbacks = optIn.callbackForwarding || [];
412
469
  const consumerCallbacks = featureConfig.callbackForwarding || [];
413
- // Union of both (author defaults + consumer additions)
414
470
  const allCallbacks = [...new Set([...authorCallbacks, ...consumerCallbacks])];
415
471
  if (allCallbacks.length > 0) {
416
472
  installCallbackForwarding(ctr, key, allCallbacks);
417
473
  }
418
- // 7. Call static onAssigned if the spawn class defines it
419
- const SpawnClass = featureConfig.spawn;
420
- if (SpawnClass && !isAsyncSpawn(SpawnClass) &&
421
- Object.hasOwn(SpawnClass, 'onAssigned') &&
422
- typeof SpawnClass.onAssigned === 'function') {
423
- const result = SpawnClass.onAssigned(ctr, featureConfig, key);
424
- if (result && typeof result.then === 'function') {
425
- onAssignedPromises.push(result);
426
- }
427
- }
428
474
  }
429
- // 8. Install whenFeatureReady method if featuresConfig.lifecycleKeys is configured
430
475
  const featuresConfig = ctr.featuresConfig;
431
476
  if (featuresConfig?.lifecycleKeys) {
432
477
  const methodName = resolveWhenFeatureReadyName(featuresConfig.lifecycleKeys);
@@ -434,10 +479,6 @@ export function assignFeatures(ctr, features, featuresRegistry) {
434
479
  installWhenFeatureReadyMethod(ctr, methodName);
435
480
  }
436
481
  }
437
- // Return a Promise if any onAssigned hooks are async, otherwise undefined
438
- if (onAssignedPromises.length > 0) {
439
- return Promise.all(onAssignedPromises).then(() => { });
440
- }
441
482
  return undefined;
442
483
  }
443
484
  /**
package/assignFeatures.ts CHANGED
@@ -470,17 +470,93 @@ export function assignFeatures(
470
470
  );
471
471
  }
472
472
 
473
- const onAssignedPromises: Promise<void>[] = [];
473
+ let hasAsync = false;
474
474
 
475
+ async function processFeatures() {
476
+ for (const key of Object.keys(features)) {
477
+ // 1. Confirm the key is opted-in via supportedFeatures
478
+ if (!(key in supportedFeatures!)) {
479
+ throw new Error(
480
+ `assignFeatures: "${key}" is not declared in ${ctr.name || 'constructor'}.supportedFeatures`
481
+ );
482
+ }
483
+
484
+ // 2. Check that the prototype doesn't already have this property defined
485
+ const existingDescriptor = Object.getOwnPropertyDescriptor(ctr.prototype, key);
486
+ if (existingDescriptor) {
487
+ throw new Error(
488
+ `assignFeatures: "${key}" already exists on ${ctr.name || 'constructor'}.prototype`
489
+ );
490
+ }
491
+
492
+ // 3. Check that this key hasn't already been registered for this constructor
493
+ if (featuresRegistry.hasKey(ctr, key)) {
494
+ throw new Error(
495
+ `assignFeatures: "${key}" has already been assigned for ${ctr.name || 'constructor'}`
496
+ );
497
+ }
498
+
499
+ // 4. Register the injection
500
+ featuresRegistry.set(ctr, key, features[key]);
501
+
502
+ // 5. Install the lazy getter on the prototype
503
+ installFeatureGetter(ctr, key, featuresRegistry);
504
+
505
+ // 6. Install callback forwarding if configured (merge author + consumer)
506
+ const featureConfig = features[key];
507
+ const optIn = supportedFeatures![key];
508
+ const authorCallbacks = optIn.callbackForwarding || [];
509
+ const consumerCallbacks = featureConfig.callbackForwarding || [];
510
+ // Union of both (author defaults + consumer additions)
511
+ const allCallbacks = [...new Set([...authorCallbacks, ...consumerCallbacks])];
512
+ if (allCallbacks.length > 0) {
513
+ installCallbackForwarding(ctr, key, allCallbacks);
514
+ }
515
+
516
+ // 7. Call static onAssigned if the spawn class defines it (sequentially awaited)
517
+ const SpawnClass = featureConfig.spawn;
518
+ if (SpawnClass && !isAsyncSpawn(SpawnClass) &&
519
+ Object.hasOwn(SpawnClass as any, 'onAssigned') &&
520
+ typeof (SpawnClass as any).onAssigned === 'function') {
521
+ const result = (SpawnClass as any).onAssigned(ctr, featureConfig, key);
522
+ if (result && typeof result.then === 'function') {
523
+ hasAsync = true;
524
+ await result;
525
+ }
526
+ }
527
+ }
528
+
529
+ // 8. Install whenFeatureReady method if featuresConfig.lifecycleKeys is configured
530
+ const featuresConfig: FeaturesClassConfig | undefined = (ctr as any).featuresConfig;
531
+ if (featuresConfig?.lifecycleKeys) {
532
+ const methodName = resolveWhenFeatureReadyName(featuresConfig.lifecycleKeys);
533
+ if (methodName) {
534
+ installWhenFeatureReadyMethod(ctr, methodName);
535
+ }
536
+ }
537
+ }
538
+
539
+ // Check if any feature has an async onAssigned (pre-scan)
540
+ for (const key of Object.keys(features)) {
541
+ const featureConfig = features[key];
542
+ const SpawnClass = featureConfig.spawn;
543
+ if (SpawnClass && !isAsyncSpawn(SpawnClass) &&
544
+ Object.hasOwn(SpawnClass as any, 'onAssigned') &&
545
+ typeof (SpawnClass as any).onAssigned === 'function') {
546
+ // We can't know if it's async without calling it, so always use the async path
547
+ // if any onAssigned exists
548
+ return processFeatures();
549
+ }
550
+ }
551
+
552
+ // No onAssigned hooks — run synchronously (inline the logic to avoid the async wrapper)
475
553
  for (const key of Object.keys(features)) {
476
- // 1. Confirm the key is opted-in via supportedFeatures
477
554
  if (!(key in supportedFeatures)) {
478
555
  throw new Error(
479
556
  `assignFeatures: "${key}" is not declared in ${ctr.name || 'constructor'}.supportedFeatures`
480
557
  );
481
558
  }
482
559
 
483
- // 2. Check that the prototype doesn't already have this property defined
484
560
  const existingDescriptor = Object.getOwnPropertyDescriptor(ctr.prototype, key);
485
561
  if (existingDescriptor) {
486
562
  throw new Error(
@@ -488,43 +564,25 @@ export function assignFeatures(
488
564
  );
489
565
  }
490
566
 
491
- // 3. Check that this key hasn't already been registered for this constructor
492
567
  if (featuresRegistry.hasKey(ctr, key)) {
493
568
  throw new Error(
494
569
  `assignFeatures: "${key}" has already been assigned for ${ctr.name || 'constructor'}`
495
570
  );
496
571
  }
497
572
 
498
- // 4. Register the injection
499
573
  featuresRegistry.set(ctr, key, features[key]);
500
-
501
- // 5. Install the lazy getter on the prototype
502
574
  installFeatureGetter(ctr, key, featuresRegistry);
503
575
 
504
- // 6. Install callback forwarding if configured (merge author + consumer)
505
576
  const featureConfig = features[key];
506
577
  const optIn = supportedFeatures[key];
507
578
  const authorCallbacks = optIn.callbackForwarding || [];
508
579
  const consumerCallbacks = featureConfig.callbackForwarding || [];
509
- // Union of both (author defaults + consumer additions)
510
580
  const allCallbacks = [...new Set([...authorCallbacks, ...consumerCallbacks])];
511
581
  if (allCallbacks.length > 0) {
512
582
  installCallbackForwarding(ctr, key, allCallbacks);
513
583
  }
514
-
515
- // 7. Call static onAssigned if the spawn class defines it
516
- const SpawnClass = featureConfig.spawn;
517
- if (SpawnClass && !isAsyncSpawn(SpawnClass) &&
518
- Object.hasOwn(SpawnClass as any, 'onAssigned') &&
519
- typeof (SpawnClass as any).onAssigned === 'function') {
520
- const result = (SpawnClass as any).onAssigned(ctr, featureConfig, key);
521
- if (result && typeof result.then === 'function') {
522
- onAssignedPromises.push(result);
523
- }
524
- }
525
584
  }
526
585
 
527
- // 8. Install whenFeatureReady method if featuresConfig.lifecycleKeys is configured
528
586
  const featuresConfig: FeaturesClassConfig | undefined = (ctr as any).featuresConfig;
529
587
  if (featuresConfig?.lifecycleKeys) {
530
588
  const methodName = resolveWhenFeatureReadyName(featuresConfig.lifecycleKeys);
@@ -533,10 +591,6 @@ export function assignFeatures(
533
591
  }
534
592
  }
535
593
 
536
- // Return a Promise if any onAssigned hooks are async, otherwise undefined
537
- if (onAssignedPromises.length > 0) {
538
- return Promise.all(onAssignedPromises).then(() => {});
539
- }
540
594
  return undefined;
541
595
  }
542
596
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assign-gingerly",
3
- "version": "0.0.47",
3
+ "version": "0.0.48",
4
4
  "description": "This package provides a utility function for carefully merging one object into another.",
5
5
  "homepage": "https://github.com/bahrus/assign-gingerly#readme",
6
6
  "bugs": {