Skip to content

Commit

Permalink
Merge pull request #19171 from emberjs/invoke-helper
Browse files Browse the repository at this point in the history
[FEAT] Implements invokeHelper
  • Loading branch information
rwjblue committed Oct 1, 2020
2 parents 66fb044 + fc2157b commit b0bc40f
Show file tree
Hide file tree
Showing 9 changed files with 796 additions and 39 deletions.
2 changes: 1 addition & 1 deletion packages/@ember/-internals/glimmer/index.ts
Expand Up @@ -406,7 +406,7 @@ export { OutletState } from './lib/utils/outlet';
export { setComponentManager, setModifierManager, setHelperManager } from './lib/utils/managers';
export { capabilities } from './lib/component-managers/custom';
export { capabilities as modifierCapabilities } from './lib/modifiers/custom';
export { helperCapabilities, HelperManager } from './lib/helpers/custom';
export { helperCapabilities, HelperManager, invokeHelper } 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';
35 changes: 27 additions & 8 deletions packages/@ember/-internals/glimmer/lib/helper.ts
Expand Up @@ -2,12 +2,13 @@
@module @ember/component
*/

import { Factory } from '@ember/-internals/owner';
import { Factory, Owner, setOwner } from '@ember/-internals/owner';
import { FrameworkObject } from '@ember/-internals/runtime';
import { getDebugName, symbol } from '@ember/-internals/utils';
import { join } from '@ember/runloop';
import { DEBUG } from '@glimmer/env';
import { Arguments, Dict } from '@glimmer/interfaces';
import { _WeakSet as WeakSet } from '@glimmer/util';
import {
consumeTag,
createTag,
Expand Down Expand Up @@ -38,6 +39,12 @@ export interface SimpleHelper<T = unknown> {
compute: HelperFunction<T>;
}

const CLASSIC_HELPER_MANAGERS = new WeakSet();

export function isClassicHelperManager(obj: object) {
return CLASSIC_HELPER_MANAGERS.has(obj);
}

/**
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 @@ -145,9 +152,21 @@ class ClassicHelperManager implements HelperManager<ClassicHelperStateBucket> {
hasDestroyable: true,
});

createHelper(definition: ClassHelperFactory, args: Arguments) {
private ownerInjection: object;

constructor(owner: Owner | undefined) {
CLASSIC_HELPER_MANAGERS.add(this);
let ownerInjection = {};
setOwner(ownerInjection, owner!);
this.ownerInjection = ownerInjection;
}

createHelper(definition: ClassHelperFactory | typeof Helper, args: Arguments) {
let instance =
definition.class === undefined ? definition.create(this.ownerInjection) : definition.create();

return {
instance: definition.create(),
instance,
args,
};
}
Expand Down Expand Up @@ -178,9 +197,7 @@ class ClassicHelperManager implements HelperManager<ClassicHelperStateBucket> {
}
}

export const CLASSIC_HELPER_MANAGER = new ClassicHelperManager();

setHelperManager(() => CLASSIC_HELPER_MANAGER, Helper);
setHelperManager((owner) => new ClassicHelperManager(owner), Helper);

///////////

Expand All @@ -203,19 +220,21 @@ class SimpleClassicHelperManager implements HelperManager<() => unknown> {
});

createHelper(definition: Wrapper, args: Arguments) {
let { compute } = definition;

if (DEBUG) {
return () => {
let ret;

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

return ret;
};
}

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

getValue(fn: () => unknown) {
Expand Down
100 changes: 97 additions & 3 deletions packages/@ember/-internals/glimmer/lib/helpers/custom.ts
@@ -1,9 +1,20 @@
import { getOwner } from '@ember/-internals/owner';
import { getDebugName } from '@ember/-internals/utils';
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 {
associateDestroyableChild,
EMPTY_ARGS,
EMPTY_NAMED,
EMPTY_POSITIONAL,
isDestroyed,
isDestroying,
} from '@glimmer/runtime';
import { Cache, createCache, getValue } from '@glimmer/validator';
import { argsProxyFor } from '../utils/args-proxy';
import { buildCapabilities, InternalCapabilities } from '../utils/managers';
import { buildCapabilities, getHelperManager, InternalCapabilities } from '../utils/managers';

export type HelperDefinition = object;

Expand Down Expand Up @@ -63,12 +74,95 @@ function hasDestroyable(manager: HelperManager): manager is HelperManagerWithDes
return manager.capabilities.hasDestroyable;
}

let ARGS_CACHES = DEBUG ? new WeakMap<SimpleArgsProxy, Cache<Partial<Arguments>>>() : undefined;

function getArgs(proxy: SimpleArgsProxy): Partial<Arguments> {
return getValue(DEBUG ? ARGS_CACHES!.get(proxy)! : proxy.argsCache!)!;
}

class SimpleArgsProxy {
argsCache?: Cache<Partial<Arguments>>;

constructor(
context: object,
computeArgs: (context: object) => Partial<Arguments> = () => EMPTY_ARGS
) {
let argsCache = createCache(() => computeArgs(context));

if (DEBUG) {
ARGS_CACHES!.set(this, argsCache);
Object.freeze(this);
} else {
this.argsCache = argsCache;
}
}

get named() {
return getArgs(this).named || EMPTY_NAMED;
}

get positional() {
return getArgs(this).positional || EMPTY_POSITIONAL;
}
}

export function invokeHelper(
context: object,
definition: HelperDefinition,
computeArgs: (context: object) => Partial<Arguments>
): Cache<unknown> {
assert(
`Expected a context object to be passed as the first parameter to invokeHelper, got ${context}`,
context !== null && typeof context === 'object'
);

const owner = getOwner(context);
const manager = getHelperManager(owner, definition)!;

// TODO: figure out why assert isn't using the TS assert thing
assert(
`Expected a helper definition to be passed as the second parameter to invokeHelper, but no helper manager was found. The definition value that was passed was \`${getDebugName!(
definition
)}\`. Did you use setHelperManager to associate a helper manager with this value?`,
manager
);

let args = new SimpleArgsProxy(context, computeArgs);
let bucket = manager.createHelper(definition, args);

let cache: Cache<unknown>;

if (hasValue(manager)) {
cache = createCache(() => {
assert(
`You attempted to get the value of a helper after the helper was destroyed, which is not allowed`,
!isDestroying(cache) && !isDestroyed(cache)
);

return manager.getValue(bucket);
});

associateDestroyableChild(context, cache);
} else {
throw new Error('TODO: unreachable, to be implemented with hasScheduledEffect');
}

if (hasDestroyable(manager)) {
let destroyable = manager.getDestroyable(bucket);

associateDestroyableChild(cache, destroyable);
}

return cache;
}

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

if (hasDestroyable(manager)) {
vm.associateDestroyable(manager.getDestroyable(bucket));
Expand Down
6 changes: 3 additions & 3 deletions packages/@ember/-internals/glimmer/lib/resolver.ts
Expand Up @@ -26,9 +26,9 @@ import { InternalComponentDefinition, isInternalManager } from './component-mana
import { TemplateOnlyComponentDefinition } from './component-managers/template-only';
import InternalComponent from './components/internal';
import {
CLASSIC_HELPER_MANAGER,
HelperFactory,
HelperInstance,
isClassicHelperManager,
SIMPLE_CLASSIC_HELPER_MANAGER,
SimpleHelper,
} from './helper';
Expand Down Expand Up @@ -384,14 +384,14 @@ export default class RuntimeResolverImpl implements RuntimeResolver<OwnedTemplat
assert(
'helper managers have not been enabled yet, you must use classic helpers',
EMBER_GLIMMER_HELPER_MANAGER ||
manager === CLASSIC_HELPER_MANAGER ||
isClassicHelperManager(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);
return customHelper(manager, isClassicHelperManager(manager) ? factory : factory.class);
}

private _lookupPartial(name: string, meta: OwnedTemplateMeta): PartialDefinition {
Expand Down
63 changes: 39 additions & 24 deletions packages/@ember/-internals/glimmer/lib/utils/managers.ts
Expand Up @@ -16,18 +16,27 @@ type ManagerDelegate =

const COMPONENT_MANAGERS = new WeakMap<
object,
ManagerFactory<ComponentManagerDelegate<unknown> | InternalComponentManager>
ManagerFactory<Owner, ComponentManagerDelegate<unknown> | InternalComponentManager>
>();

const FROM_CAPABILITIES = DEBUG ? new _WeakSet() : undefined;

const MODIFIER_MANAGERS = new WeakMap<object, ManagerFactory<ModifierManagerDelegate<unknown>>>();
const MODIFIER_MANAGERS = new WeakMap<
object,
ManagerFactory<Owner, ModifierManagerDelegate<unknown>>
>();

const HELPER_MANAGERS = new WeakMap<object, ManagerFactory<HelperManager<unknown>>>();
const HELPER_MANAGERS = new WeakMap<
object,
ManagerFactory<Owner | undefined, HelperManager<unknown>>
>();

const MANAGER_INSTANCES: WeakMap<Owner, WeakMap<ManagerFactory, unknown>> = new WeakMap();
const OWNER_MANAGER_INSTANCES: WeakMap<Owner, WeakMap<ManagerFactory, unknown>> = new WeakMap();
const UNDEFINED_MANAGER_INSTANCES: WeakMap<ManagerFactory, unknown> = new WeakMap();

export type ManagerFactory<D extends ManagerDelegate = ManagerDelegate> = (owner: Owner) => D;
export type ManagerFactory<O = Owner, D extends ManagerDelegate = ManagerDelegate> = (
owner: O
) => D;

///////////

Expand All @@ -42,10 +51,10 @@ function setManager<Def extends object>(
return obj;
}

function getManager<D extends ManagerDelegate>(
map: WeakMap<object, ManagerFactory<D>>,
function getManager<O, D extends ManagerDelegate>(
map: WeakMap<object, ManagerFactory<O, D>>,
obj: object
): ManagerFactory<D> | undefined {
): ManagerFactory<O, D> | undefined {
let pointer = obj;
while (pointer !== undefined && pointer !== null) {
const manager = map.get(pointer);
Expand All @@ -61,21 +70,26 @@ function getManager<D extends ManagerDelegate>(
}

function getManagerInstanceForOwner<D extends ManagerDelegate>(
owner: Owner,
factory: ManagerFactory<D>
owner: Owner | undefined,
factory: ManagerFactory<Owner, D>
): D {
let managers = MANAGER_INSTANCES.get(owner);
let managers;

if (managers === undefined) {
managers = new WeakMap();
MANAGER_INSTANCES.set(owner, managers);
if (owner === undefined) {
managers = UNDEFINED_MANAGER_INSTANCES;
} else {
managers = OWNER_MANAGER_INSTANCES.get(owner);

if (managers === undefined) {
managers = new WeakMap();
OWNER_MANAGER_INSTANCES.set(owner, managers);
}
}

let instance = managers.get(factory);

if (instance === undefined) {
instance = factory(owner);

instance = factory(owner!);
managers.set(factory, instance!);
}

Expand All @@ -86,14 +100,14 @@ function getManagerInstanceForOwner<D extends ManagerDelegate>(
///////////

export function setModifierManager(
factory: ManagerFactory<ModifierManagerDelegate<unknown>>,
factory: ManagerFactory<Owner, ModifierManagerDelegate<unknown>>,
definition: object
) {
return setManager(MODIFIER_MANAGERS, factory, definition);
}

export function getModifierManager(
owner: Owner,
owner: Owner | undefined,
definition: object
): ModifierManagerDelegate<unknown> | undefined {
const factory = getManager(MODIFIER_MANAGERS, definition);
Expand All @@ -114,14 +128,14 @@ export function getModifierManager(
}

export function setHelperManager(
factory: ManagerFactory<HelperManager<unknown>>,
factory: ManagerFactory<Owner | undefined, HelperManager<unknown>>,
definition: object
) {
return setManager(HELPER_MANAGERS, factory, definition);
}

export function getHelperManager(
owner: Owner,
owner: Owner | undefined,
definition: object
): HelperManager<unknown> | undefined {
const factory = getManager(HELPER_MANAGERS, definition);
Expand All @@ -145,10 +159,10 @@ export function getHelperManager(
export function setComponentManager(
stringOrFunction:
| string
| ManagerFactory<ComponentManagerDelegate<unknown> | InternalComponentManager>,
| ManagerFactory<Owner, ComponentManagerDelegate<unknown> | InternalComponentManager>,
obj: object
) {
let factory: ManagerFactory<ComponentManagerDelegate<unknown> | InternalComponentManager>;
let factory: ManagerFactory<Owner, ComponentManagerDelegate<unknown> | InternalComponentManager>;
if (COMPONENT_MANAGER_STRING_LOOKUP && typeof stringOrFunction === 'string') {
deprecate(
'Passing the name of the component manager to "setupComponentManager" is deprecated. Please pass a function that produces an instance of the manager.',
Expand All @@ -166,6 +180,7 @@ export function setComponentManager(
};
} else {
factory = stringOrFunction as ManagerFactory<
Owner,
ComponentManagerDelegate<unknown> | InternalComponentManager
>;
}
Expand All @@ -174,10 +189,10 @@ export function setComponentManager(
}

export function getComponentManager(
owner: Owner,
owner: Owner | undefined,
definition: object
): ComponentManagerDelegate<unknown> | InternalComponentManager | undefined {
const factory = getManager<ComponentManagerDelegate<unknown> | InternalComponentManager>(
const factory = getManager<Owner, ComponentManagerDelegate<unknown> | InternalComponentManager>(
COMPONENT_MANAGERS,
definition
);
Expand Down

0 comments on commit b0bc40f

Please sign in to comment.