Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT] Implements invokeHelper #19171

Merged
merged 1 commit into from
Oct 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/@ember/-internals/glimmer/index.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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;
pzuraq marked this conversation as resolved.
Show resolved Hide resolved

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

class SimpleArgsProxy {
Copy link
Sponsor Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does something like this already exist for modifiers?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a more complicated version which proxies each individual argument. Using that would require users to pass in each arg as a function, which would be a bit prohibitive and is not what was spec'd. So, this one is a simpler proxy that basically calls the users compute function and then returns the proper value whenever positional or named is accessed.

pzuraq marked this conversation as resolved.
Show resolved Hide resolved
argsCache?: Cache<Partial<Arguments>>;

constructor(
pzuraq marked this conversation as resolved.
Show resolved Hide resolved
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)
);
pzuraq marked this conversation as resolved.
Show resolved Hide resolved

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);
pzuraq marked this conversation as resolved.
Show resolved Hide resolved
}

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);
pzuraq marked this conversation as resolved.
Show resolved Hide resolved

if (hasDestroyable(manager)) {
vm.associateDestroyable(manager.getDestroyable(bucket));
Expand Down
6 changes: 3 additions & 3 deletions packages/@ember/-internals/glimmer/lib/resolver.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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!);
Copy link
Sponsor Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you know that owner exists here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may actually be undefined, in the case of helper managers, but for modifier/component managers it will always exist at this point (since you are not currently allowed to pass an undefined owner to getModifierManager or getComponentManager). The types here are a bit messy, but I think it should be fine.

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
Loading