Skip to content

Commit

Permalink
[FEAT] Implements Helper Managers
Browse files Browse the repository at this point in the history
Implements the helper manager feature specified in RFC 625. Highlights:

1. Adds `getDebugName` to the interface for helper managers. This is an
   optional hook that is used for better logging purposes, and matches
   other internal APIs we've added recently.

2. `hasScheduledEffect` has not yet been implemented, and attempting to
   use it will cause an assertion to be thrown.

Helper managers are not exposed with this PR, and the version passed to
`helperCapabilities` is optimistic, but can be changed when we do expose
them (along with an appropriate feature flag).

Co-authored-by: Robert Jackson <[email protected]>
  • Loading branch information
Chris Garrett and rwjblue committed Sep 29, 2020
1 parent aeef923 commit f15ca15
Show file tree
Hide file tree
Showing 11 changed files with 562 additions and 90 deletions.
8 changes: 2 additions & 6 deletions packages/@ember/-internals/glimmer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,14 +403,10 @@ export { default as AbstractComponentManager } from './lib/component-managers/ab
export { INVOKE } from './lib/helpers/action';
export { default as OutletView } from './lib/views/outlet';
export { OutletState } from './lib/utils/outlet';
export { setComponentManager, setModifierManager, setHelperManager } from './lib/utils/managers';
export { capabilities } from './lib/component-managers/custom';
export {
setComponentManager,
getComponentManager,
setModifierManager,
getModifierManager,
} from './lib/utils/managers';
export { capabilities as modifierCapabilities } from './lib/modifiers/custom';
export { helperCapabilities, HelperManager } from './lib/helpers/custom';
export { isSerializationFirstNode } from './lib/utils/serialization-first-node-helpers';
export { setComponentTemplate, getComponentTemplate } from './lib/utils/component-template';
export { CapturedRenderNode } from './lib/utils/debug-render-tree';
111 changes: 96 additions & 15 deletions packages/@ember/-internals/glimmer/lib/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,19 @@

import { Factory } from '@ember/-internals/owner';
import { FrameworkObject } from '@ember/-internals/runtime';
import { symbol } from '@ember/-internals/utils';
import { getDebugName, symbol } from '@ember/-internals/utils';
import { join } from '@ember/runloop';
import { Dict } from '@glimmer/interfaces';
import { createTag, dirtyTag } from '@glimmer/validator';
import { DEBUG } from '@glimmer/env';
import { Arguments, Dict } from '@glimmer/interfaces';
import { _WeakSet as WeakSet } from '@glimmer/util';
import {
consumeTag,
createTag,
deprecateMutationsInTrackingTransaction,
dirtyTag,
} from '@glimmer/validator';
import { helperCapabilities, HelperManager } from './helpers/custom';
import { setHelperManager } from './utils/managers';

export const RECOMPUTE_TAG = symbol('RECOMPUTE_TAG');

Expand All @@ -30,18 +39,6 @@ export interface SimpleHelper<T = unknown> {
compute: HelperFunction<T>;
}

export function isHelperFactory(
helper: any | undefined | null
): helper is Factory<SimpleHelper | HelperInstance, HelperFactory<SimpleHelper | HelperInstance>> {
return (
typeof helper === 'object' && helper !== null && helper.class && helper.class.isHelperFactory
);
}

export function isClassHelper(helper: SimpleHelper | HelperInstance): helper is HelperInstance {
return (helper as any).destroy !== undefined;
}

/**
Ember Helpers are functions that can compute values, and are used in templates.
For example, this code calls a helper named `format-currency`:
Expand Down Expand Up @@ -138,6 +135,56 @@ let Helper = FrameworkObject.extend({

Helper.isHelperFactory = true;

interface ClassicHelperStateBucket {
instance: HelperInstance;
args: Arguments;
}

class ClassicHelperManager implements HelperManager<ClassicHelperStateBucket> {
capabilities = helperCapabilities('3.23', {
hasValue: true,
hasDestroyable: true,
});

createHelper(definition: ClassHelperFactory, args: Arguments) {
return {
instance: definition.create(),
args,
};
}

getDestroyable({ instance }: ClassicHelperStateBucket) {
return instance;
}

getValue({ instance, args }: ClassicHelperStateBucket) {
let ret;
let { positional, named } = args;

if (DEBUG) {
deprecateMutationsInTrackingTransaction!(() => {
ret = instance.compute(positional, named);
});
} else {
ret = instance.compute(positional, named);
}

consumeTag(instance[RECOMPUTE_TAG]);

return ret;
}

getDebugName(definition: ClassHelperFactory) {
return getDebugName!(definition.class!['prototype']);
}
}

export const CLASSIC_HELPER_MANAGER = new ClassicHelperManager();

setHelperManager(() => CLASSIC_HELPER_MANAGER, Helper);

///////////

class Wrapper implements HelperFactory<SimpleHelper> {
isHelperFactory: true = true;

Expand All @@ -151,6 +198,40 @@ class Wrapper implements HelperFactory<SimpleHelper> {
}
}

class SimpleClassicHelperManager implements HelperManager<() => unknown> {
capabilities = helperCapabilities('3.23', {
hasValue: true,
});

createHelper(definition: Wrapper, args: Arguments) {
if (DEBUG) {
return () => {
let ret;

deprecateMutationsInTrackingTransaction!(() => {
ret = definition.compute.call(null, args.positional, args.named);
});

return ret;
};
}

return definition.compute.bind(null, args.positional, args.named);
}

getValue(fn: () => unknown) {
return fn();
}

getDebugName(definition: Wrapper) {
return getDebugName!(definition.compute);
}
}

export const SIMPLE_CLASSIC_HELPER_MANAGER = new SimpleClassicHelperManager();

setHelperManager(() => SIMPLE_CLASSIC_HELPER_MANAGER, Wrapper.prototype);

/**
In many cases it is not necessary to use the full `Helper` class.
The `helper` method create pure-function helpers without instances.
Expand Down
86 changes: 86 additions & 0 deletions packages/@ember/-internals/glimmer/lib/helpers/custom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { assert } from '@ember/debug';
import { DEBUG } from '@glimmer/env';
import { Arguments, Helper as GlimmerHelper } from '@glimmer/interfaces';
import { createComputeRef, UNDEFINED_REFERENCE } from '@glimmer/reference';
import { argsProxyFor } from '../utils/args-proxy';

export type HelperDefinition = object;

export interface HelperCapabilities {
hasValue: boolean;
hasDestroyable: boolean;
hasScheduledEffect: boolean;
}

export function helperCapabilities(
managerAPI: string,
options: Partial<HelperCapabilities> = {}
): HelperCapabilities {
assert('Invalid helper manager compatibility specified', managerAPI === '3.23');

assert(
'You must pass either the `hasValue` OR the `hasScheduledEffect` capability when defining a helper manager. Passing neither, or both, is not permitted.',
(options.hasValue || options.hasScheduledEffect) &&
!(options.hasValue && options.hasScheduledEffect)
);

assert(
'The `hasScheduledEffect` capability has not yet been implemented for helper managers. Please pass `hasValue` instead',
!options.hasScheduledEffect
);

return {
hasValue: Boolean(options.hasValue),
hasDestroyable: Boolean(options.hasDestroyable),
hasScheduledEffect: Boolean(options.hasScheduledEffect),
};
}

export interface HelperManager<HelperStateBucket = unknown> {
capabilities: HelperCapabilities;

createHelper(definition: HelperDefinition, args: Arguments): HelperStateBucket;

getDebugName?(definition: HelperDefinition): string;
}

export interface HelperManagerWithValue<HelperStateBucket = unknown>
extends HelperManager<HelperStateBucket> {
getValue(bucket: HelperStateBucket): unknown;
}

function hasValue(manager: HelperManager): manager is HelperManagerWithValue {
return manager.capabilities.hasValue;
}

export interface HelperManagerWithDestroyable<HelperStateBucket = unknown>
extends HelperManager<HelperStateBucket> {
getDestroyable(bucket: HelperStateBucket): object;
}

function hasDestroyable(manager: HelperManager): manager is HelperManagerWithDestroyable {
return manager.capabilities.hasDestroyable;
}

export default function customHelper(
manager: HelperManager<unknown>,
definition: HelperDefinition
): GlimmerHelper {
return (args, vm) => {
const bucket = manager.createHelper(definition, argsProxyFor(args.capture(), 'helper'));

if (hasDestroyable(manager)) {
vm.associateDestroyable(manager.getDestroyable(bucket));
}

if (hasValue(manager)) {
return createComputeRef(
() => manager.getValue(bucket),
null,
DEBUG && manager.getDebugName && manager.getDebugName(definition)
);
} else {
return UNDEFINED_REFERENCE;
}
};
}
62 changes: 35 additions & 27 deletions packages/@ember/-internals/glimmer/lib/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import { privatize as P } from '@ember/-internals/container';
import { ENV } from '@ember/-internals/environment';
import { Factory, FactoryClass, LookupOptions, Owner } from '@ember/-internals/owner';
import { OwnedTemplateMeta } from '@ember/-internals/views';
import { EMBER_GLIMMER_SET_COMPONENT_TEMPLATE } from '@ember/canary-features';
import {
EMBER_GLIMMER_HELPER_MANAGER,
EMBER_GLIMMER_SET_COMPONENT_TEMPLATE,
} from '@ember/canary-features';
import { isTemplateOnlyComponent } from '@ember/component/template-only';
import { assert, deprecate } from '@ember/debug';
import { PARTIALS } from '@ember/deprecated-features';
import EmberError from '@ember/error';
import { _instrumentStart } from '@ember/instrumentation';
import { DEBUG } from '@glimmer/env';
import {
ComponentDefinition,
Helper,
Expand All @@ -17,20 +19,27 @@ import {
RuntimeResolver,
} from '@glimmer/interfaces';
import { PartialDefinitionImpl } from '@glimmer/opcode-compiler';
import { getDynamicVar, ModifierDefinition, registerDestructor } from '@glimmer/runtime';
import { getDynamicVar, ModifierDefinition } from '@glimmer/runtime';
import { CurlyComponentDefinition } from './component-managers/curly';
import { CustomManagerDefinition } from './component-managers/custom';
import { InternalComponentDefinition, isInternalManager } from './component-managers/internal';
import { TemplateOnlyComponentDefinition } from './component-managers/template-only';
import InternalComponent from './components/internal';
import { isClassHelper, isHelperFactory } from './helper';
import {
CLASSIC_HELPER_MANAGER,
HelperFactory,
HelperInstance,
SIMPLE_CLASSIC_HELPER_MANAGER,
SimpleHelper,
} from './helper';
import { default as componentAssertionHelper } from './helpers/-assert-implicit-component-helper-argument';
import { default as inElementNullCheckHelper } from './helpers/-in-element-null-check';
import { default as normalizeClassHelper } from './helpers/-normalize-class';
import { default as trackArray } from './helpers/-track-array';
import { default as action } from './helpers/action';
import { default as array } from './helpers/array';
import { default as concat } from './helpers/concat';
import customHelper from './helpers/custom';
import { default as eachIn } from './helpers/each-in';
import { default as fn } from './helpers/fn';
import { default as get } from './helpers/get';
Expand All @@ -48,8 +57,7 @@ import { mountHelper } from './syntax/mount';
import { outletHelper } from './syntax/outlet';
import { Factory as TemplateFactory, OwnedTemplate } from './template';
import { getComponentTemplate } from './utils/component-template';
import { getComponentManager, getModifierManager } from './utils/managers';
import { createHelperRef } from './utils/references';
import { getComponentManager, getHelperManager, getModifierManager } from './utils/managers';

function instrumentationPayload(name: string) {
return { object: `component:${name}` };
Expand Down Expand Up @@ -358,32 +366,32 @@ export default class RuntimeResolverImpl implements RuntimeResolver<OwnedTemplat
const options: LookupOptions = makeOptions(moduleName, namespace);

const factory =
owner.factoryFor(`helper:${name}`, options) || owner.factoryFor(`helper:${name}`);
owner.factoryFor<SimpleHelper | HelperInstance, HelperFactory<SimpleHelper | HelperInstance>>(
`helper:${name}`,
options
) || owner.factoryFor(`helper:${name}`);

if (!isHelperFactory(factory)) {
if (factory === undefined || factory.class === undefined) {
return null;
}

return (args, vm) => {
const helper = factory.create();

if (isClassHelper(helper)) {
let helperDestroyable = {};

// Do this so that `destroy` gets called correctly
registerDestructor(helperDestroyable, () => helper.destroy(), true);
vm.associateDestroyable(helperDestroyable);
} else if (DEBUG) {
// Bind to null in case someone accidentally passed an unbound function
// in, and attempts use `this` on it.
//
// TODO: Update buildUntouchableThis to be flexible enough to provide a
// nice error message here.
helper.compute = helper.compute.bind(null);
}
const manager = getHelperManager(owner, factory.class);

return createHelperRef(helper, args.capture());
};
if (manager === undefined) {
return null;
}

assert(
'helper managers have not been enabled yet, you must use classic helpers',
EMBER_GLIMMER_HELPER_MANAGER ||
manager === CLASSIC_HELPER_MANAGER ||
manager === SIMPLE_CLASSIC_HELPER_MANAGER
);

// For classic class based helpers, we need to pass the factoryFor result itself rather
// than the raw value (`factoryFor(...).class`). This is because injections are already
// bound in the factoryFor result, including type-based injections
return customHelper(manager, CLASSIC_HELPER_MANAGER === manager ? factory : factory.class);
}

private _lookupPartial(name: string, meta: OwnedTemplateMeta): PartialDefinition {
Expand Down
Loading

0 comments on commit f15ca15

Please sign in to comment.