function makeAutoSubscribeDecorator()

in src/AutoSubscriptions.ts [213:320]


function makeAutoSubscribeDecorator(shallow = false, autoSubscribeKeys?: string[]): MethodDecorator {
    return <T>(target: InstanceTarget, methodName: string|symbol, descriptor: TypedPropertyDescriptor<T>) => {
        const methodNameString = methodName.toString();
        const targetWithMetadata = instanceTargetToInstanceTargetWithMetadata(target);
        const metaForMethod = getMethodMetadata(targetWithMetadata, methodNameString);

        // Record that the target is decorated.
        metaForMethod.hasAutoSubscribeDecorator = true;

        // Save the method being decorated. Note this might not be the original method if already decorated.
        // Note: T might have other properties (e.g. T = { (): void; bar: number; }). We don't support that and need a cast/assert.
        const existingMethod = descriptor.value as any as Function;
        assert(isFunction(existingMethod), 'Can only use @autoSubscribe on methods');

        // Note: we need to be given 'this', so cannot use '=>' syntax.
        descriptor.value = function AutoSubscribe(this: any, ...args: any[]) {
            assert(targetWithMetadata.__resubMetadata.__decorated, `Missing @AutoSubscribeStore class decorator: "${ methodNameString }"`);

            if (Options.development) {
                // This is a check to see if we're in a rendering function component function.  If you are, then calling useState will
                // noop.  If you aren't, then useState will throw an exception.  So, we want to make sure that either you're inside render
                // and have the call going through a wrapped component, or that you're not inside render, and hence calling the getter
                // from a store or service or other random non-lifecycled instance, so it's on you to figure out how to manage
                // subscriptions in that instance.
                let inRender = false;
                try {
                    useState();
                    inRender = true;
                } catch {
                    // I guess we weren't in render.
                }

                assert(!inRender || !!handlerWrapper, 'Autosubscribe method called from inside a render function ' +
                    'or function component without using withResubAutoSubscriptions');
            }

            // Just call the method if no handler is setup.
            const scopedHandleWrapper = handlerWrapper;
            if (!scopedHandleWrapper || scopedHandleWrapper.useAutoSubscriptions === AutoOptions.None) {
                return existingMethod.apply(this, args);
            }

            // If this is forbidding auto-subscribe then do not go through the auto-subscribe path below.
            if (scopedHandleWrapper.useAutoSubscriptions === AutoOptions.Forbid) {
                assert(false, `Only Store methods WITHOUT the ` +
                    `@autoSubscribe decorator can be called right now (e.g. in render): "${ methodNameString }"`);

                return existingMethod.apply(this, args);
            }

            // Try to find an @key parameter in the target's metadata and form initial Key(s) from it/them.
            let keyParamValues: (string | number)[] = [];
            if (metaForMethod.keyIndexes) {
                keyParamValues = metaForMethod.keyIndexes.map(index => {
                    let keyArg: number | string = args[index];

                    if (isNumber(keyArg)) {
                        keyArg = keyArg.toString();
                    }

                    assert(keyArg, `@key parameter must be given a non-empty string or number: ` +
                        `"${ methodNameString }"@${ index } was given ${ JSON.stringify(keyArg) }`);

                    assert(isString(keyArg), `@key parameter must be given a string or number: ` +
                        `"${ methodNameString }"@${ index }`);

                    return keyArg;
                });
            }

            // Form a list of keys to trigger.
            // If we have @key values, put them first, then append the @autosubscribewithkey key to the end.
            // If there are multiple keys in the @autosubscribewithkey list, go through each one and do the
            // same thing (@key then value).  If there's neither @key nor @autosubscribewithkey, it's Key_All.
            const specificKeyValues: string[] = (autoSubscribeKeys && autoSubscribeKeys.length > 0) ?
                autoSubscribeKeys.map(autoSubKey => formCompoundKey(...keyParamValues.concat(autoSubKey))) :
                [(keyParamValues.length > 0) ? formCompoundKey(...keyParamValues) : StoreBase.Key_All];

            // Let the handler know about this auto-subscriptions, then proceed to the existing method.
            let wasInAutoSubscribe: boolean;
            const result = _tryFinally(() => {
                // Disable further auto-subscriptions if shallow.
                scopedHandleWrapper.useAutoSubscriptions = shallow ? AutoOptions.None : AutoOptions.Enabled;
                // Any further @warnIfAutoSubscribeEnabled methods are safe.
                wasInAutoSubscribe = scopedHandleWrapper.inAutoSubscribe;
                scopedHandleWrapper.inAutoSubscribe = true;

                // Let the handler know about this auto-subscription.
                for (const specificKeyValue of specificKeyValues) {
                    scopedHandleWrapper
                        .handler!!!
                        .handle
                        .apply(scopedHandleWrapper.instance, [scopedHandleWrapper.instance, this, specificKeyValue]);
                }

                return existingMethod.apply(this, args);
            }, () => {
                // Must have been previously enabled to reach here.
                scopedHandleWrapper.useAutoSubscriptions = AutoOptions.Enabled;
                scopedHandleWrapper.inAutoSubscribe = wasInAutoSubscribe;
            });

            return result;
        } as any as T;

        return descriptor;
    };
}