assign-gingerly 0.0.73 → 0.0.75
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/README.md +29 -92
- package/assignFeatures.js +145 -254
- package/assignFeatures.ts +713 -837
- package/defineWithFeatures.js +12 -72
- package/defineWithFeatures.ts +92 -167
- package/enhanceAll.js +8 -4
- package/enhanceAll.ts +8 -7
- package/inferencer/types/assign-gingerly/types.d.ts +2 -16
- package/inferencer/types/font-face-feature/types.d.ts +9 -0
- package/package.json +10 -1
- package/processHandlerCommands.js +11 -34
- package/processHandlerCommands.ts +13 -40
- package/resolveAndAssignFeatures.js +9 -40
- package/resolveAndAssignFeatures.ts +43 -79
- package/types/assign-gingerly/types.d.ts +2 -16
- package/utils/findClassPrototypeInPath.js +57 -0
- package/utils/findClassPrototypeInPath.ts +62 -0
- package/utils/isAsyncSpawn.js +20 -0
- package/utils/isAsyncSpawn.ts +17 -0
package/assignFeatures.js
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* properties installed on the class prototype.
|
|
10
10
|
*/
|
|
11
11
|
import { parseWithAttrs } from './parseWithAttrs.js';
|
|
12
|
+
import { isAsyncSpawn } from './utils/isAsyncSpawn.js';
|
|
13
|
+
import { findClassPrototypeInPath } from './utils/findClassPrototypeInPath.js';
|
|
12
14
|
/**
|
|
13
15
|
* WeakMap storing per-instance feature caches.
|
|
14
16
|
* Outer key: the instance (element or other object).
|
|
@@ -44,69 +46,79 @@ export class FeaturesRegistry {
|
|
|
44
46
|
*/
|
|
45
47
|
const RAW_INIT_VALS = Symbol('rawInitVals');
|
|
46
48
|
/**
|
|
47
|
-
*
|
|
49
|
+
* Cache for resolved feature spawns.
|
|
50
|
+
* Key: target constructor, Value: Map<featureKey, resolvedConstructor>
|
|
48
51
|
*/
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
* Returns undefined if lifecycleKeys is not set.
|
|
58
|
-
*/
|
|
59
|
-
function resolveWhenFeatureReadyName(lifecycleKeys) {
|
|
60
|
-
if (lifecycleKeys === undefined)
|
|
61
|
-
return undefined;
|
|
62
|
-
if (lifecycleKeys === true)
|
|
63
|
-
return 'whenFeatureReady';
|
|
64
|
-
return lifecycleKeys.whenFeatureReady || 'whenFeatureReady';
|
|
52
|
+
export const resolvedSpawnCache = new WeakMap();
|
|
53
|
+
function getOrCreateClassCache(ctr) {
|
|
54
|
+
let classCache = resolvedSpawnCache.get(ctr);
|
|
55
|
+
if (!classCache) {
|
|
56
|
+
classCache = new Map();
|
|
57
|
+
resolvedSpawnCache.set(ctr, classCache);
|
|
58
|
+
}
|
|
59
|
+
return classCache;
|
|
65
60
|
}
|
|
66
61
|
/**
|
|
67
|
-
*
|
|
62
|
+
* Resolves a spawn reference (constructor, async function, or import-path string).
|
|
68
63
|
*/
|
|
69
|
-
function
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (pending) {
|
|
80
|
-
return pending.promise;
|
|
81
|
-
}
|
|
82
|
-
// No pending — feature is already resolved (sync or async already completed)
|
|
83
|
-
return Promise.resolve(current);
|
|
84
|
-
},
|
|
85
|
-
writable: true,
|
|
86
|
-
enumerable: false,
|
|
87
|
-
configurable: true
|
|
88
|
-
});
|
|
64
|
+
async function resolveSpawn(spawnish) {
|
|
65
|
+
if (!spawnish)
|
|
66
|
+
return undefined;
|
|
67
|
+
if (typeof spawnish === 'string') {
|
|
68
|
+
return await findClassPrototypeInPath(spawnish);
|
|
69
|
+
}
|
|
70
|
+
if (isAsyncSpawn(spawnish)) {
|
|
71
|
+
return await spawnish();
|
|
72
|
+
}
|
|
73
|
+
return spawnish;
|
|
89
74
|
}
|
|
90
75
|
/**
|
|
91
|
-
*
|
|
92
|
-
* rather than a synchronous constructor.
|
|
93
|
-
*
|
|
94
|
-
* Heuristic:
|
|
95
|
-
* - AsyncFunction (async () => ...) → async spawner
|
|
96
|
-
* - Arrow function (no .prototype) → async spawner (assumed to return Promise<Constructor>)
|
|
97
|
-
* - Class or function declaration (has .prototype) → synchronous constructor
|
|
76
|
+
* Returns true if resolving this spawn reference requires awaiting.
|
|
98
77
|
*/
|
|
99
|
-
function
|
|
100
|
-
if (typeof
|
|
101
|
-
return false;
|
|
102
|
-
// Explicit async function
|
|
103
|
-
if (fn.constructor.name === 'AsyncFunction')
|
|
78
|
+
function isAsyncResolution(spawnish) {
|
|
79
|
+
if (typeof spawnish === 'string')
|
|
104
80
|
return true;
|
|
105
|
-
|
|
106
|
-
if (fn.prototype === undefined)
|
|
81
|
+
if (isAsyncSpawn(spawnish))
|
|
107
82
|
return true;
|
|
108
83
|
return false;
|
|
109
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Resolves all configured spawns for the target constructor and caches them.
|
|
87
|
+
* Returns a Promise if any resolution is async, otherwise undefined.
|
|
88
|
+
*/
|
|
89
|
+
function resolveAndCacheSpawns(ctr, features, supportedFeatures) {
|
|
90
|
+
let hasAsync = false;
|
|
91
|
+
const asyncResolutions = [];
|
|
92
|
+
for (const key of Object.keys(features)) {
|
|
93
|
+
const classCache = getOrCreateClassCache(ctr);
|
|
94
|
+
if (classCache.has(key))
|
|
95
|
+
continue;
|
|
96
|
+
const featureConfig = features[key];
|
|
97
|
+
let spawnish = featureConfig.spawn;
|
|
98
|
+
if (spawnish === undefined) {
|
|
99
|
+
const optIn = supportedFeatures[key];
|
|
100
|
+
spawnish = optIn?.fallbackSpawn;
|
|
101
|
+
}
|
|
102
|
+
if (!spawnish) {
|
|
103
|
+
classCache.set(key, undefined);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (isAsyncResolution(spawnish)) {
|
|
107
|
+
hasAsync = true;
|
|
108
|
+
asyncResolutions.push((async () => {
|
|
109
|
+
const resolved = await resolveSpawn(spawnish);
|
|
110
|
+
classCache.set(key, resolved);
|
|
111
|
+
})());
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
classCache.set(key, spawnish);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (hasAsync) {
|
|
118
|
+
return Promise.all(asyncResolutions).then(() => undefined);
|
|
119
|
+
}
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
110
122
|
/**
|
|
111
123
|
* Installs a getter/setter pair on the constructor's prototype for the given feature key.
|
|
112
124
|
*
|
|
@@ -129,10 +141,6 @@ function installFeatureGetter(ctr, key, featuresRegistry) {
|
|
|
129
141
|
featureStorage.set(this, storage);
|
|
130
142
|
}
|
|
131
143
|
const stored = storage.get(key);
|
|
132
|
-
// Check for error state from failed async spawn
|
|
133
|
-
if (stored && typeof stored === 'object' && FEATURE_ERROR in stored) {
|
|
134
|
-
throw stored[FEATURE_ERROR];
|
|
135
|
-
}
|
|
136
144
|
// If already spawned (not a raw sentinel, not undefined), return it
|
|
137
145
|
if (stored !== undefined && !(stored && typeof stored === 'object' && RAW_INIT_VALS in stored)) {
|
|
138
146
|
return stored;
|
|
@@ -158,17 +166,17 @@ function installFeatureGetter(ctr, key, featuresRegistry) {
|
|
|
158
166
|
if (!injection) {
|
|
159
167
|
throw new Error(`assignFeatures: no injection found for feature "${key}"`);
|
|
160
168
|
}
|
|
161
|
-
// Resolve spawn
|
|
169
|
+
// Resolve spawn from the cache populated before getters were installed
|
|
170
|
+
const spawns = resolvedSpawnCache.get(ctr);
|
|
171
|
+
const resolvedSpawn = spawns?.get(key);
|
|
172
|
+
if (!resolvedSpawn) {
|
|
173
|
+
throw new Error(`assignFeatures: no spawn implementation found for feature "${key}"`);
|
|
174
|
+
}
|
|
162
175
|
const supportedFeatures = ctr.supportedFeatures;
|
|
163
176
|
const optIn = supportedFeatures?.[key];
|
|
164
177
|
if (!optIn) {
|
|
165
178
|
throw new Error(`assignFeatures: "${key}" not in static supportedFeatures`);
|
|
166
179
|
}
|
|
167
|
-
const SpawnClass = injection.spawn || optIn.fallbackSpawn;
|
|
168
|
-
if (!SpawnClass) {
|
|
169
|
-
throw new Error(`assignFeatures: no spawn implementation found for feature "${key}". ` +
|
|
170
|
-
`Provide spawn in assignFeatures() or fallbackSpawn in supportedFeatures.`);
|
|
171
|
-
}
|
|
172
180
|
// Build the spawn context
|
|
173
181
|
const shared = optIn.getSharedContext?.(this);
|
|
174
182
|
const ctx = {
|
|
@@ -196,103 +204,16 @@ function installFeatureGetter(ctr, key, featuresRegistry) {
|
|
|
196
204
|
? { ...attrInitVals, ...initVals }
|
|
197
205
|
: attrInitVals;
|
|
198
206
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
// Create a pending Promise for whenFeatureReady consumers
|
|
206
|
-
let pendingMap = pendingFeatures.get(hostElement);
|
|
207
|
-
if (!pendingMap) {
|
|
208
|
-
pendingMap = new Map();
|
|
209
|
-
pendingFeatures.set(hostElement, pendingMap);
|
|
207
|
+
// Synchronous path: SpawnClass is a constructor
|
|
208
|
+
const instance = new resolvedSpawn(this, ctx, initVals);
|
|
209
|
+
// Validate shape if configured
|
|
210
|
+
if (optIn.validateShape) {
|
|
211
|
+
if (!optIn.validateShape(instance)) {
|
|
212
|
+
throw new Error(`assignFeatures: spawned instance for "${key}" failed shape validation`);
|
|
210
213
|
}
|
|
211
|
-
let resolvePending;
|
|
212
|
-
let rejectPending;
|
|
213
|
-
const promise = new Promise((resolve, reject) => {
|
|
214
|
-
resolvePending = resolve;
|
|
215
|
-
rejectPending = reject;
|
|
216
|
-
});
|
|
217
|
-
pendingMap.set(key, { promise, resolve: resolvePending, reject: rejectPending });
|
|
218
|
-
// Kick off async resolution
|
|
219
|
-
SpawnClass().then((ResolvedClass) => {
|
|
220
|
-
// Mutate injection so future getter calls see the resolved constructor
|
|
221
|
-
injection.spawn = ResolvedClass;
|
|
222
|
-
// Get the current placeholder (may have accumulated properties via assignGingerly)
|
|
223
|
-
const currentStorage = featureStorage.get(hostElement);
|
|
224
|
-
const currentPlaceholder = currentStorage?.get(key);
|
|
225
|
-
// Don't upgrade if an error was stored or if already upgraded
|
|
226
|
-
if (!currentPlaceholder || (typeof currentPlaceholder === 'object' && FEATURE_ERROR in currentPlaceholder)) {
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
// Parse attributes at resolution time (element should be in DOM by now)
|
|
230
|
-
let asyncAttrInitVals = undefined;
|
|
231
|
-
if (injection.withAttrs && hostElement instanceof Element) {
|
|
232
|
-
try {
|
|
233
|
-
asyncAttrInitVals = parseWithAttrs(hostElement, injection.withAttrs, true // always unprefixed for features
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
catch (e) {
|
|
237
|
-
// Non-fatal: log and continue with placeholder as initVals
|
|
238
|
-
console.error('Error parsing feature attributes during async resolution:', e);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
// Merge: attributes are base, placeholder (programmatic) overrides
|
|
242
|
-
const asyncInitVals = asyncAttrInitVals
|
|
243
|
-
? { ...asyncAttrInitVals, ...currentPlaceholder }
|
|
244
|
-
: currentPlaceholder;
|
|
245
|
-
// Instantiate the real class with merged initVals
|
|
246
|
-
const realCtx = {
|
|
247
|
-
key,
|
|
248
|
-
optIn,
|
|
249
|
-
injection,
|
|
250
|
-
featuresRegistry: fr,
|
|
251
|
-
shared: optIn.getSharedContext?.(hostElement)
|
|
252
|
-
};
|
|
253
|
-
const instance = new ResolvedClass(hostElement, realCtx, asyncInitVals);
|
|
254
|
-
// Validate shape if configured
|
|
255
|
-
if (optIn.validateShape) {
|
|
256
|
-
if (!optIn.validateShape(instance)) {
|
|
257
|
-
const error = new Error(`assignFeatures: spawned instance for "${key}" failed shape validation`);
|
|
258
|
-
error.placeholder = currentPlaceholder;
|
|
259
|
-
currentStorage.set(key, { [FEATURE_ERROR]: error });
|
|
260
|
-
rejectPending(error);
|
|
261
|
-
pendingMap.delete(key);
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
// Replace placeholder with real instance
|
|
266
|
-
currentStorage.set(key, instance);
|
|
267
|
-
// Resolve the pending Promise and clean up
|
|
268
|
-
resolvePending(instance);
|
|
269
|
-
pendingMap.delete(key);
|
|
270
|
-
}).catch((err) => {
|
|
271
|
-
// Store error state — getter will throw on next access
|
|
272
|
-
const currentStorage = featureStorage.get(hostElement);
|
|
273
|
-
const currentPlaceholder = currentStorage?.get(key);
|
|
274
|
-
const error = new Error(`assignFeatures: async spawn for "${key}" failed: ${err.message}`);
|
|
275
|
-
error.placeholder = currentPlaceholder;
|
|
276
|
-
error.cause = err;
|
|
277
|
-
currentStorage?.set(key, { [FEATURE_ERROR]: error });
|
|
278
|
-
// Reject the pending Promise and clean up
|
|
279
|
-
rejectPending(error);
|
|
280
|
-
pendingMap.delete(key);
|
|
281
|
-
});
|
|
282
|
-
return placeholder;
|
|
283
|
-
}
|
|
284
|
-
else {
|
|
285
|
-
// Synchronous path: SpawnClass is a constructor
|
|
286
|
-
const instance = new SpawnClass(this, ctx, initVals);
|
|
287
|
-
// Validate shape if configured
|
|
288
|
-
if (optIn.validateShape) {
|
|
289
|
-
if (!optIn.validateShape(instance)) {
|
|
290
|
-
throw new Error(`assignFeatures: spawned instance for "${key}" failed shape validation`);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
storage.set(key, instance);
|
|
294
|
-
return instance;
|
|
295
214
|
}
|
|
215
|
+
storage.set(key, instance);
|
|
216
|
+
return instance;
|
|
296
217
|
},
|
|
297
218
|
enumerable: true,
|
|
298
219
|
configurable: false
|
|
@@ -350,10 +271,9 @@ function installCallbackForwarding(ctr, key, callbacks) {
|
|
|
350
271
|
for (const featureKey of keys) {
|
|
351
272
|
// Access the getter (triggers lazy spawn on first connectedCallback)
|
|
352
273
|
const feature = this[featureKey];
|
|
353
|
-
// Only forward if it's a real instance
|
|
274
|
+
// Only forward if it's a real instance
|
|
354
275
|
if (feature && typeof feature === 'object' &&
|
|
355
|
-
typeof feature[callbackName] === 'function'
|
|
356
|
-
!(FEATURE_ERROR in feature)) {
|
|
276
|
+
typeof feature[callbackName] === 'function') {
|
|
357
277
|
feature[callbackName](...args);
|
|
358
278
|
}
|
|
359
279
|
}
|
|
@@ -368,9 +288,66 @@ function installCallbackForwarding(ctr, key, callbacks) {
|
|
|
368
288
|
featureKeys.add(key);
|
|
369
289
|
}
|
|
370
290
|
}
|
|
291
|
+
function installOneFeature(ctr, key, features, supportedFeatures, featuresRegistry) {
|
|
292
|
+
// 1. Confirm the key is opted-in via supportedFeatures
|
|
293
|
+
if (!(key in supportedFeatures)) {
|
|
294
|
+
throw new Error(`assignFeatures: "${key}" is not declared in ${ctr.name || 'constructor'}.supportedFeatures`);
|
|
295
|
+
}
|
|
296
|
+
// 2. Check that the prototype doesn't already have this property defined
|
|
297
|
+
const existingDescriptor = Object.getOwnPropertyDescriptor(ctr.prototype, key);
|
|
298
|
+
if (existingDescriptor) {
|
|
299
|
+
throw new Error(`assignFeatures: "${key}" already exists on ${ctr.name || 'constructor'}.prototype`);
|
|
300
|
+
}
|
|
301
|
+
// 3. Check that this key hasn't already been registered for this constructor
|
|
302
|
+
if (featuresRegistry.hasKey(ctr, key)) {
|
|
303
|
+
throw new Error(`assignFeatures: "${key}" has already been assigned for ${ctr.name || 'constructor'}`);
|
|
304
|
+
}
|
|
305
|
+
// 4. Register the injection
|
|
306
|
+
featuresRegistry.set(ctr, key, features[key]);
|
|
307
|
+
// 5. Install the lazy getter on the prototype
|
|
308
|
+
installFeatureGetter(ctr, key, featuresRegistry);
|
|
309
|
+
// 6. Install callback forwarding if configured (merge author + consumer)
|
|
310
|
+
const featureConfig = features[key];
|
|
311
|
+
const optIn = supportedFeatures[key];
|
|
312
|
+
const authorCallbacks = optIn.callbackForwarding || [];
|
|
313
|
+
const consumerCallbacks = featureConfig.callbackForwarding || [];
|
|
314
|
+
// Union of both (author defaults + consumer additions)
|
|
315
|
+
const allCallbacks = [...new Set([...authorCallbacks, ...consumerCallbacks])];
|
|
316
|
+
if (allCallbacks.length > 0) {
|
|
317
|
+
installCallbackForwarding(ctr, key, allCallbacks);
|
|
318
|
+
}
|
|
319
|
+
// 7. Call static onAssigned if the resolved spawn class defines it
|
|
320
|
+
const classCache = getOrCreateClassCache(ctr);
|
|
321
|
+
const resolvedSpawn = classCache.get(key);
|
|
322
|
+
if (resolvedSpawn && !isAsyncSpawn(resolvedSpawn) &&
|
|
323
|
+
Object.hasOwn(resolvedSpawn, 'onAssigned') &&
|
|
324
|
+
typeof resolvedSpawn.onAssigned === 'function') {
|
|
325
|
+
const result = resolvedSpawn.onAssigned(ctr, featureConfig, key);
|
|
326
|
+
if (result && typeof result.then === 'function') {
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
function installAllFeatures(ctr, features, supportedFeatures, featuresRegistry) {
|
|
333
|
+
let asyncResult;
|
|
334
|
+
for (const key of Object.keys(features)) {
|
|
335
|
+
const result = installOneFeature(ctr, key, features, supportedFeatures, featuresRegistry);
|
|
336
|
+
if (result) {
|
|
337
|
+
if (!asyncResult) {
|
|
338
|
+
asyncResult = result.then(() => undefined);
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
asyncResult = asyncResult.then(() => result.then(() => undefined));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return asyncResult;
|
|
346
|
+
}
|
|
371
347
|
/**
|
|
372
348
|
* Core assignFeatures implementation.
|
|
373
|
-
* Validates inputs,
|
|
349
|
+
* Validates inputs, resolves all configured spawns (async if needed), caches them,
|
|
350
|
+
* and installs lazy getters on the prototype.
|
|
374
351
|
*
|
|
375
352
|
* Important: Call assignFeatures BEFORE customElements.define(), or at minimum
|
|
376
353
|
* before any instances of the element are created. The lazy getters must be on
|
|
@@ -386,100 +363,14 @@ export function assignFeatures(ctr, features, featuresRegistry) {
|
|
|
386
363
|
if (!supportedFeatures) {
|
|
387
364
|
throw new Error(`assignFeatures: ${ctr.name || 'constructor'} does not define static supportedFeatures`);
|
|
388
365
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
}
|
|
366
|
+
// Resolve all configured spawns before installing getters. This is the single
|
|
367
|
+
// source of truth for spawn resolution; other callers (defineWithFeatures, etc.)
|
|
368
|
+
// delegate to assignFeatures.
|
|
369
|
+
const spawnResolution = resolveAndCacheSpawns(ctr, features, supportedFeatures);
|
|
370
|
+
if (spawnResolution) {
|
|
371
|
+
return spawnResolution.then(() => installAllFeatures(ctr, features, supportedFeatures, featuresRegistry));
|
|
451
372
|
}
|
|
452
|
-
|
|
453
|
-
for (const key of Object.keys(features)) {
|
|
454
|
-
if (!(key in supportedFeatures)) {
|
|
455
|
-
throw new Error(`assignFeatures: "${key}" is not declared in ${ctr.name || 'constructor'}.supportedFeatures`);
|
|
456
|
-
}
|
|
457
|
-
const existingDescriptor = Object.getOwnPropertyDescriptor(ctr.prototype, key);
|
|
458
|
-
if (existingDescriptor) {
|
|
459
|
-
throw new Error(`assignFeatures: "${key}" already exists on ${ctr.name || 'constructor'}.prototype`);
|
|
460
|
-
}
|
|
461
|
-
if (featuresRegistry.hasKey(ctr, key)) {
|
|
462
|
-
throw new Error(`assignFeatures: "${key}" has already been assigned for ${ctr.name || 'constructor'}`);
|
|
463
|
-
}
|
|
464
|
-
featuresRegistry.set(ctr, key, features[key]);
|
|
465
|
-
installFeatureGetter(ctr, key, featuresRegistry);
|
|
466
|
-
const featureConfig = features[key];
|
|
467
|
-
const optIn = supportedFeatures[key];
|
|
468
|
-
const authorCallbacks = optIn.callbackForwarding || [];
|
|
469
|
-
const consumerCallbacks = featureConfig.callbackForwarding || [];
|
|
470
|
-
const allCallbacks = [...new Set([...authorCallbacks, ...consumerCallbacks])];
|
|
471
|
-
if (allCallbacks.length > 0) {
|
|
472
|
-
installCallbackForwarding(ctr, key, allCallbacks);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
const featuresConfig = ctr.featuresConfig;
|
|
476
|
-
if (featuresConfig?.lifecycleKeys) {
|
|
477
|
-
const methodName = resolveWhenFeatureReadyName(featuresConfig.lifecycleKeys);
|
|
478
|
-
if (methodName) {
|
|
479
|
-
installWhenFeatureReadyMethod(ctr, methodName);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
return undefined;
|
|
373
|
+
return installAllFeatures(ctr, features, supportedFeatures, featuresRegistry);
|
|
483
374
|
}
|
|
484
375
|
/**
|
|
485
376
|
* Captures own-properties that shadow feature getters and stores them as initVals.
|