diff --git a/packages/@glimmer/integration-tests/index.ts b/packages/@glimmer/integration-tests/index.ts index b908e4732a..026dd71906 100644 --- a/packages/@glimmer/integration-tests/index.ts +++ b/packages/@glimmer/integration-tests/index.ts @@ -17,3 +17,4 @@ export * from './lib/suites'; export * from './lib/test-helpers/module'; export * from './lib/test-helpers/strings'; export * from './lib/test-helpers/test'; +export * from './lib/test-helpers/tracked'; diff --git a/packages/@glimmer/integration-tests/lib/modifiers.ts b/packages/@glimmer/integration-tests/lib/modifiers.ts index dfa8096eb3..692862b8aa 100644 --- a/packages/@glimmer/integration-tests/lib/modifiers.ts +++ b/packages/@glimmer/integration-tests/lib/modifiers.ts @@ -3,12 +3,11 @@ import { Dict, ModifierManager, GlimmerTreeChanges, - Destroyable, DynamicScope, VMArguments, CapturedArguments, } from '@glimmer/interfaces'; -import { Tag } from '@glimmer/validator'; +import { Tag, consumeTag } from '@glimmer/validator'; export interface TestModifierConstructor { new (): TestModifierInstance; @@ -22,12 +21,7 @@ export interface TestModifierInstance { } export class TestModifierDefinitionState { - instance?: TestModifierInstance; - constructor(Klass?: TestModifierConstructor) { - if (Klass) { - this.instance = new Klass(); - } - } + constructor(public Class: TestModifierConstructor) {} } export class TestModifierManager @@ -39,46 +33,48 @@ export class TestModifierManager _dynamicScope: DynamicScope, dom: GlimmerTreeChanges ) { - return new TestModifier(element, state, args.capture(), dom); + let { Class } = state; + + return new TestModifier(element, new Class(), args.capture(), dom); } getTag({ args: { tag } }: TestModifier): Tag { return tag; } - install({ element, args, state }: TestModifier) { - if (state.instance && state.instance.didInsertElement) { - state.instance.element = element; - state.instance.didInsertElement(args.positional.value(), args.named.value()); + install({ element, args, instance }: TestModifier) { + consumeTag(args.tag); + + if (instance && instance.didInsertElement) { + instance.element = element; + instance.didInsertElement(args.positional.value(), args.named.value()); } return; } - update({ args, state }: TestModifier) { - if (state.instance && state.instance.didUpdate) { - state.instance.didUpdate(args.positional.value(), args.named.value()); + update({ args, instance }: TestModifier) { + consumeTag(args.tag); + + if (instance && instance.didUpdate) { + instance.didUpdate(args.positional.value(), args.named.value()); } return; } - getDestructor(modifier: TestModifier): Destroyable { - return { - destroy: () => { - let { state } = modifier; - if (state.instance && state.instance.willDestroyElement) { - state.instance.willDestroyElement(); - } - }, - }; + teardown(modifier: TestModifier): void { + let { instance } = modifier; + if (instance && instance.willDestroyElement) { + instance.willDestroyElement(); + } } } export class TestModifier { constructor( public element: SimpleElement, - public state: TestModifierDefinitionState, + public instance: TestModifierInstance, public args: CapturedArguments, public dom: GlimmerTreeChanges ) {} diff --git a/packages/@glimmer/integration-tests/test/modifiers-test.ts b/packages/@glimmer/integration-tests/test/modifiers-test.ts index 770951f133..d4112303a2 100644 --- a/packages/@glimmer/integration-tests/test/modifiers-test.ts +++ b/packages/@glimmer/integration-tests/test/modifiers-test.ts @@ -1,4 +1,4 @@ -import { RenderTest, jitSuite, test, Count } from '..'; +import { RenderTest, jitSuite, test, Count, tracked, EmberishGlimmerComponent, EmberishGlimmerArgs } from '..'; import { Dict } from '@glimmer/interfaces'; import { SimpleElement } from '@simple-dom/interface'; import { Option } from '@glimmer/interfaces'; @@ -302,6 +302,72 @@ class ModifierTests extends RenderTest { assert.deepEqual(elementIds, ['outer-div', 'inner-div'], 'Modifiers are called on all levels'); } + @test + 'child modifiers that are added later update before parents'(assert: Assert) { + let inserts: Option[] = []; + let updates: Option[] = []; + + class Bar extends BaseModifier { + didInsertElement(params: unknown[]) { + assert.deepEqual(params, [123]); + if (this.element) { + inserts.push(this.element.getAttribute('id')); + } + } + + didUpdate(params: unknown[]) { + assert.deepEqual(params, [124]); + if (this.element) { + updates.push(this.element.getAttribute('id')); + } + } + } + + let component: Option = null; + + class Foo extends EmberishGlimmerComponent { + constructor(args: EmberishGlimmerArgs) { + super(args); + + component = this; + } + + @tracked showing = false; + @tracked count = 123; + } + + this.registerComponent( + 'Glimmer', + 'Foo', + ` +
+ {{#if this.showing}} +
+ {{/if}} +
+ `, + Foo + ); + this.registerModifier('bar', Bar); + + this.render(''); + + assert.deepEqual(inserts, ['outer'], 'outer modifier insert called'); + assert.deepEqual(updates, [], 'no updates called'); + + component!.showing = true; + this.rerender(); + + assert.deepEqual(inserts, ['outer', 'inner'], 'inner modifier insert called'); + assert.deepEqual(updates, [], 'no updates called'); + + component!.count++; + this.rerender(); + + assert.deepEqual(inserts, ['outer', 'inner'], 'inserts not called'); + assert.deepEqual(updates, ['inner', 'outer'], 'updates called in correct order'); + } + @test 'same element insertion order'(assert: Assert) { let insertionOrder: string[] = []; diff --git a/packages/@glimmer/interfaces/lib/runtime/environment.d.ts b/packages/@glimmer/interfaces/lib/runtime/environment.d.ts index 1346626d09..c076463625 100644 --- a/packages/@glimmer/interfaces/lib/runtime/environment.d.ts +++ b/packages/@glimmer/interfaces/lib/runtime/environment.d.ts @@ -8,7 +8,7 @@ import { AttributeOperation } from '../dom/attributes'; import { AttrNamespace, SimpleElement, SimpleDocument } from '@simple-dom/interface'; import { ComponentInstanceState } from '../components'; import { ComponentManager } from '../components/component-manager'; -import { Drop, Option } from '../core'; +import { Drop, Option, SymbolDestroyable } from '../core'; import { GlimmerTreeChanges, GlimmerTreeConstruction } from '../dom/changes'; import { ModifierManager } from './modifier'; @@ -26,6 +26,10 @@ export interface Transaction {} declare const TransactionSymbol: unique symbol; export type TransactionSymbol = typeof TransactionSymbol; +export interface Effect extends SymbolDestroyable { + createOrUpdate(): void; +} + export interface Environment { [TransactionSymbol]: Option; @@ -35,8 +39,7 @@ export interface Environment { willDestroy(drop: Drop): void; didDestroy(drop: Drop): void; - scheduleInstallModifier(modifier: unknown, manager: ModifierManager): void; - scheduleUpdateModifier(modifier: unknown, manager: ModifierManager): void; + registerEffect(phase: 'layout', effect: Effect): void; begin(): void; commit(): void; diff --git a/packages/@glimmer/interfaces/lib/runtime/modifier.d.ts b/packages/@glimmer/interfaces/lib/runtime/modifier.d.ts index a23bef8393..d1b7cab5dd 100644 --- a/packages/@glimmer/interfaces/lib/runtime/modifier.d.ts +++ b/packages/@glimmer/interfaces/lib/runtime/modifier.d.ts @@ -34,7 +34,7 @@ export interface ModifierManager< // Convert the opaque token into an object that implements Destroyable. // If it returns null, the modifier will not be destroyed. - getDestructor(modifier: ModifierInstanceState): Option; + teardown(modifier: ModifierInstanceState): void; } export interface ModifierDefinition< diff --git a/packages/@glimmer/runtime/index.ts b/packages/@glimmer/runtime/index.ts index 27da6ccf6c..fe00bfcf96 100644 --- a/packages/@glimmer/runtime/index.ts +++ b/packages/@glimmer/runtime/index.ts @@ -25,13 +25,12 @@ export { isWhitespace, } from './lib/dom/helper'; export { normalizeProperty } from './lib/dom/props'; -export { DefaultDynamicScope } from './lib/dynamic-scope'; +export { PartialScopeImpl, DefaultDynamicScope } from './lib/scope'; export { AotRuntime, JitRuntime, EnvironmentImpl, EnvironmentDelegate, - ScopeImpl, JitProgramCompilationContext, JitSyntaxCompilationContext, inTransaction, diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/-debug-strip.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/-debug-strip.ts index c83b391558..6cc1463f62 100644 --- a/packages/@glimmer/runtime/lib/compiled/opcodes/-debug-strip.ts +++ b/packages/@glimmer/runtime/lib/compiled/opcodes/-debug-strip.ts @@ -28,7 +28,7 @@ import { } from '@glimmer/interfaces'; import { VersionedPathReference, Reference } from '@glimmer/reference'; import { Tag, COMPUTE } from '@glimmer/validator'; -import { ScopeImpl } from '../../environment'; +import { PartialScopeImpl } from '../../scope'; import CurryComponentReference from '../../references/curry-component'; import { CapturedNamedArgumentsImpl, @@ -99,7 +99,9 @@ export const CheckCapturedArguments: Checker = CheckInterface export const CheckCurryComponent = wrap(() => CheckInstanceof(CurryComponentReference)); -export const CheckScope: Checker> = wrap(() => CheckInstanceof(ScopeImpl)); +export const CheckScope: Checker> = wrap(() => + CheckInstanceof(PartialScopeImpl) +); export const CheckComponentManager: Checker> = CheckInterface({ getCapabilities: CheckFunction, diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts index 2e09d53911..fc741b56f8 100644 --- a/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts +++ b/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts @@ -1,20 +1,9 @@ import { Reference, ReferenceCache, VersionedReference } from '@glimmer/reference'; -import { - Revision, - Tag, - isConstTagged, - isConstTag, - valueForTag, - validateTag, -} from '@glimmer/validator'; +import { Revision, Tag, isConstTagged, valueForTag, validateTag } from '@glimmer/validator'; import { check, CheckString, CheckElement, CheckOption, CheckNode } from '@glimmer/debug'; import { Op, Option, ModifierManager } from '@glimmer/interfaces'; import { $t0 } from '@glimmer/vm'; -import { - ModifierDefinition, - InternalModifierManager, - ModifierInstanceState, -} from '../../modifier/interfaces'; +import { ModifierDefinition } from '../../modifier/interfaces'; import { APPEND_OPCODES, UpdatingOpcode } from '../../opcodes'; import { UpdatingVM } from '../../vm'; import { Assert } from './vm'; @@ -23,6 +12,7 @@ import { CheckReference, CheckArguments, CheckOperations } from './-debug-strip' import { CONSTANTS } from '../../symbols'; import { SimpleElement, SimpleNode } from '@simple-dom/interface'; import { expect, Maybe } from '@glimmer/util'; +import { EffectImpl } from '../../effects'; APPEND_OPCODES.add(Op.Text, (vm, { op1: text }) => { vm.elements().appendText(vm[CONSTANTS].getString(text)); @@ -93,12 +83,22 @@ APPEND_OPCODES.add(Op.CloseElement, vm => { if (modifiers) { modifiers.forEach(([manager, modifier]) => { - vm.env.scheduleInstallModifier(modifier, manager); - let d = manager.getDestructor(modifier); - - if (d) { - vm.associateDestroyable(d); - } + let effect = new EffectImpl({ + setup() { + manager.install(modifier); + }, + + update() { + manager.update(modifier); + }, + + teardown() { + manager.teardown(modifier); + }, + }); + + vm.env.registerEffect('layout', effect); + vm.associateDestroyable(effect); }); } }); @@ -123,37 +123,8 @@ APPEND_OPCODES.add(Op.Modifier, (vm, { op1: handle }) => { ); operations.addModifier(manager, modifier); - - let tag = manager.getTag(modifier); - - if (!isConstTag(tag)) { - vm.updateWith(new UpdateModifierOpcode(tag, manager, modifier)); - } }); -export class UpdateModifierOpcode extends UpdatingOpcode { - public type = 'update-modifier'; - private lastUpdated: Revision; - - constructor( - public tag: Tag, - private manager: InternalModifierManager, - private modifier: ModifierInstanceState - ) { - super(); - this.lastUpdated = valueForTag(tag); - } - - evaluate(vm: UpdatingVM) { - let { manager, modifier, tag, lastUpdated } = this; - - if (!validateTag(tag, lastUpdated)) { - vm.env.scheduleUpdateModifier(modifier, manager); - this.lastUpdated = valueForTag(tag); - } - } -} - APPEND_OPCODES.add(Op.StaticAttr, (vm, { op1: _name, op2: _value, op3: _namespace }) => { let name = vm[CONSTANTS].getString(_name); let value = vm[CONSTANTS].getString(_value); diff --git a/packages/@glimmer/runtime/lib/dynamic-scope.ts b/packages/@glimmer/runtime/lib/dynamic-scope.ts deleted file mode 100644 index 5f72f7ee2a..0000000000 --- a/packages/@glimmer/runtime/lib/dynamic-scope.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DynamicScope, Dict } from '@glimmer/interfaces'; -import { assign } from '@glimmer/util'; -import { PathReference } from '@glimmer/reference'; - -export class DefaultDynamicScope implements DynamicScope { - private bucket: Dict; - - constructor(bucket?: Dict) { - if (bucket) { - this.bucket = assign({}, bucket); - } else { - this.bucket = {}; - } - } - - get(key: string): PathReference { - return this.bucket[key]; - } - - set(key: string, reference: PathReference): PathReference { - return (this.bucket[key] = reference); - } - - child(): DefaultDynamicScope { - return new DefaultDynamicScope(this.bucket); - } -} diff --git a/packages/@glimmer/runtime/lib/effects.ts b/packages/@glimmer/runtime/lib/effects.ts new file mode 100644 index 0000000000..e370c4e11f --- /dev/null +++ b/packages/@glimmer/runtime/lib/effects.ts @@ -0,0 +1,122 @@ +import { Effect } from '@glimmer/interfaces'; +import { LinkedList, ListNode, DESTROY, associate, assert, Option } from '@glimmer/util'; +import { memo } from '@glimmer/validator'; +import { DEBUG } from '@glimmer/env'; + +const effectPhases = ['layout'] as const; +export type EffectPhase = typeof effectPhases[number]; + +interface EffectHooks { + setup(): void; + update(): void; + teardown(): void; +} + +export class EffectImpl implements Effect { + constructor(private hooks: EffectHooks) {} + + private didSetup = false; + + createOrUpdate = memo(() => { + if (this.didSetup === false) { + this.didSetup = true; + this.hooks.setup(); + } else { + this.hooks.update(); + } + }); + + [DESTROY]() { + this.hooks.teardown(); + } +} + +function defaultScheduleEffects(_phase: EffectPhase, callback: () => void) { + callback(); +} + +class EffectQueue { + /** + * The effects in this queue + */ + effects: LinkedList> = new LinkedList(); + + /** + * Tracker for the current head of the queue. This is used to coordinate + * adding effects to the queue. In a given render pass, all effects added + * should be added in the order they were received, but they should be + * _prepended_ to any pre-existing effects. For instance, let's say that the + * queue started off in this state after our first render pass: + * + * A, B, C + * + * On the next render pass, we add D and E, which are siblings, and children + * of A. The timing semantics of effects is that siblings should be + * initialized in the order they were defined in, and should run before + * parents. So, assuming D comes before E, we want to do: + * + * D, E, A, B, C + * + * This way, new children will always run in the correct order, and before + * their parents. By keeping track of the current head at the beginning of a + * transaction, we can insert new effects in the proper order during the + * transaction. + */ + currentHead: Option> = null; + + revalidate = () => this.effects.forEachNode(n => n.value.createOrUpdate()); +} + +export class EffectManager { + private inTransaction = false; + + constructor(private scheduleEffects = defaultScheduleEffects) { + let queues: Record = {}; + + for (let phase of effectPhases) { + queues[phase] = new EffectQueue(); + } + + this.queues = queues as { [key in EffectPhase]: EffectQueue }; + } + + private queues: { [key in EffectPhase]: EffectQueue }; + + begin() { + if (DEBUG) { + this.inTransaction = true; + } + } + + registerEffect(phase: EffectPhase, effect: Effect) { + assert(this.inTransaction, 'You cannot register effects unless you are in a transaction'); + + let queue = this.queues[phase]; + let effects = queue.effects; + let newNode = new ListNode(effect); + + effects.insertBefore(newNode, queue.currentHead); + + associate(effect, { + [DESTROY]() { + effects.remove(newNode); + }, + }); + } + + commit() { + if (DEBUG) { + this.inTransaction = false; + } + + let { queues, scheduleEffects } = this; + + for (let phase of effectPhases) { + let queue = queues[phase]; + + scheduleEffects(phase, queue.revalidate); + + queue.currentHead = queue.effects.head(); + } + } +} diff --git a/packages/@glimmer/runtime/lib/environment.ts b/packages/@glimmer/runtime/lib/environment.ts index 894b0a4604..56cad617eb 100644 --- a/packages/@glimmer/runtime/lib/environment.ts +++ b/packages/@glimmer/runtime/lib/environment.ts @@ -6,18 +6,12 @@ import { EnvironmentOptions, GlimmerTreeChanges, GlimmerTreeConstruction, - JitOrAotBlock, - PartialScope, - Scope, - ScopeBlock, - ScopeSlot, Transaction, TransactionSymbol, CompilerArtifacts, WithCreateInstance, ResolvedValue, RuntimeResolverDelegate, - ModifierManager, Template, AotRuntimeResolver, Invocation, @@ -32,6 +26,7 @@ import { CompileTimeConstants, CompileTimeHeap, WholeProgramCompilationContext, + Effect, } from '@glimmer/interfaces'; import { IterableImpl, @@ -44,137 +39,19 @@ import { import { assert, WILL_DROP, DID_DROP, expect, symbol } from '@glimmer/util'; import { AttrNamespace, SimpleElement } from '@simple-dom/interface'; import { DOMChangesImpl, DOMTreeConstruction } from './dom/helper'; -import { ConditionalReference, UNDEFINED_REFERENCE } from './references'; +import { ConditionalReference } from './references'; import { DynamicAttribute, dynamicAttribute } from './vm/attributes/dynamic'; import { RuntimeProgramImpl } from '@glimmer/program'; - -export function isScopeReference(s: ScopeSlot): s is VersionedPathReference { - if (s === null || Array.isArray(s)) return false; - return true; -} - -export class ScopeImpl implements PartialScope { - static root(self: PathReference, size = 0): PartialScope { - let refs: PathReference[] = new Array(size + 1); - - for (let i = 0; i <= size; i++) { - refs[i] = UNDEFINED_REFERENCE; - } - - return new ScopeImpl(refs, null, null, null).init({ self }); - } - - static sized(size = 0): Scope { - let refs: PathReference[] = new Array(size + 1); - - for (let i = 0; i <= size; i++) { - refs[i] = UNDEFINED_REFERENCE; - } - - return new ScopeImpl(refs, null, null, null); - } - - constructor( - // the 0th slot is `self` - readonly slots: Array>, - private callerScope: Option>, - // named arguments and blocks passed to a layout that uses eval - private evalScope: Option>>, - // locals in scope when the partial was invoked - private partialMap: Option>> - ) {} - - init({ self }: { self: PathReference }): this { - this.slots[0] = self; - return this; - } - - getSelf(): PathReference { - return this.get>(0); - } - - getSymbol(symbol: number): PathReference { - return this.get>(symbol); - } - - getBlock(symbol: number): Option> { - let block = this.get(symbol); - return block === UNDEFINED_REFERENCE ? null : (block as ScopeBlock); - } - - getEvalScope(): Option>> { - return this.evalScope; - } - - getPartialMap(): Option>> { - return this.partialMap; - } - - bind(symbol: number, value: ScopeSlot) { - this.set(symbol, value); - } - - bindSelf(self: PathReference) { - this.set>(0, self); - } - - bindSymbol(symbol: number, value: PathReference) { - this.set(symbol, value); - } - - bindBlock(symbol: number, value: Option>) { - this.set>>(symbol, value); - } - - bindEvalScope(map: Option>>) { - this.evalScope = map; - } - - bindPartialMap(map: Dict>) { - this.partialMap = map; - } - - bindCallerScope(scope: Option>): void { - this.callerScope = scope; - } - - getCallerScope(): Option> { - return this.callerScope; - } - - child(): Scope { - return new ScopeImpl(this.slots.slice(), this.callerScope, this.evalScope, this.partialMap); - } - - private get>(index: number): T { - if (index >= this.slots.length) { - throw new RangeError(`BUG: cannot get $${index} from scope; length=${this.slots.length}`); - } - - return this.slots[index] as T; - } - - private set>(index: number, value: T): void { - if (index >= this.slots.length) { - throw new RangeError(`BUG: cannot get $${index} from scope; length=${this.slots.length}`); - } - - this.slots[index] = value; - } -} +import { EffectManager, EffectPhase } from './effects'; export const TRANSACTION: TransactionSymbol = symbol('TRANSACTION'); class TransactionImpl implements Transaction { - public scheduledInstallManagers: ModifierManager[] = []; - public scheduledInstallModifiers: unknown[] = []; - public scheduledUpdateModifierManagers: ModifierManager[] = []; - public scheduledUpdateModifiers: unknown[] = []; - public createdComponents: unknown[] = []; - public createdManagers: WithCreateInstance[] = []; - public updatedComponents: unknown[] = []; - public updatedManagers: WithCreateInstance[] = []; - public destructors: Drop[] = []; + private createdComponents: unknown[] = []; + private createdManagers: WithCreateInstance[] = []; + private updatedComponents: unknown[] = []; + private updatedManagers: WithCreateInstance[] = []; + private destructors: Drop[] = []; didCreate(component: unknown, manager: WithCreateInstance) { this.createdComponents.push(component); @@ -186,16 +63,6 @@ class TransactionImpl implements Transaction { this.updatedManagers.push(manager); } - scheduleInstallModifier(modifier: unknown, manager: ModifierManager) { - this.scheduledInstallModifiers.push(modifier); - this.scheduledInstallManagers.push(manager); - } - - scheduleUpdateModifier(modifier: unknown, manager: ModifierManager) { - this.scheduledUpdateModifiers.push(modifier); - this.scheduledUpdateModifierManagers.push(manager); - } - willDestroy(d: Drop) { d[WILL_DROP](); } @@ -226,22 +93,6 @@ class TransactionImpl implements Transaction { for (let i = 0; i < destructors.length; i++) { destructors[i][DID_DROP](); } - - let { scheduledInstallManagers, scheduledInstallModifiers } = this; - - for (let i = 0; i < scheduledInstallManagers.length; i++) { - let modifier = scheduledInstallModifiers[i]; - let manager = scheduledInstallManagers[i]; - manager.install(modifier); - } - - let { scheduledUpdateModifierManagers, scheduledUpdateModifiers } = this; - - for (let i = 0; i < scheduledUpdateModifierManagers.length; i++) { - let modifier = scheduledUpdateModifiers[i]; - let manager = scheduledUpdateModifierManagers[i]; - manager.update(modifier); - } } } @@ -277,6 +128,8 @@ export class EnvironmentImpl implements Environment { public toBool = defaultDelegateFn(this.delegate.toBool, defaultToBool); public toIterator = defaultDelegateFn(this.delegate.toIterator, defaultToIterator); + private effectManager = new EffectManager(this.delegate.scheduleEffects); + constructor(options: EnvironmentOptions, private delegate: EnvironmentDelegate) { if (options.appendOperations) { this.appendOperations = options.appendOperations; @@ -337,6 +190,8 @@ export class EnvironmentImpl implements Environment { this.delegate.onTransactionBegin(); } + this.effectManager.begin(); + this[TRANSACTION] = new TransactionImpl(); } @@ -352,16 +207,8 @@ export class EnvironmentImpl implements Environment { this.transaction.didUpdate(component, manager); } - scheduleInstallModifier(modifier: unknown, manager: ModifierManager) { - if (this.isInteractive) { - this.transaction.scheduleInstallModifier(modifier, manager); - } - } - - scheduleUpdateModifier(modifier: unknown, manager: ModifierManager) { - if (this.isInteractive) { - this.transaction.scheduleUpdateModifier(modifier, manager); - } + registerEffect(phase: EffectPhase, effect: Effect) { + this.effectManager.registerEffect(phase, effect); } willDestroy(d: Drop) { @@ -373,12 +220,14 @@ export class EnvironmentImpl implements Environment { } commit() { - let transaction = this.transaction; + let { transaction, delegate } = this; this[TRANSACTION] = null; transaction.commit(); - if (this.delegate.onTransactionCommit !== undefined) { - this.delegate.onTransactionCommit(); + this.effectManager.commit(); + + if (delegate.onTransactionCommit !== undefined) { + delegate.onTransactionCommit(); } } } @@ -469,6 +318,17 @@ export interface EnvironmentDelegate { namespace: Option ): DynamicAttribute; + /** + * Allows the embedding environment to schedule effects to be run in the future. + * Different phases will be passed to this callback, and each one should be + * scheduled at an appropriate time for that phase. The callback will be + * called at the end each transaction. + * + * @param phase the phase of effects that are being scheduled + * @param runEffects the callback which runs the effects + */ + scheduleEffects?: (phase: EffectPhase, runEffects: () => void) => void; + /** * Slot for any extra values that the embedding environment wants to add, * providing/passing around additional context to various users in the VM. diff --git a/packages/@glimmer/runtime/lib/opcodes.ts b/packages/@glimmer/runtime/lib/opcodes.ts index bbec6aa1f1..9761c3b13b 100644 --- a/packages/@glimmer/runtime/lib/opcodes.ts +++ b/packages/@glimmer/runtime/lib/opcodes.ts @@ -11,7 +11,7 @@ import { debug, logOpcode } from '@glimmer/debug'; import { DESTRUCTOR_STACK, INNER_VM, CONSTANTS, STACKS } from './symbols'; import { InternalVM, InternalJitVM } from './vm/append'; import { CURSOR_STACK } from './vm/element-builder'; -import { isScopeReference } from './environment'; +import { isScopeReference } from './scope'; export interface OpcodeJSON { type: number | string; diff --git a/packages/@glimmer/runtime/lib/render.ts b/packages/@glimmer/runtime/lib/render.ts index b053520913..4e9196a4a4 100644 --- a/packages/@glimmer/runtime/lib/render.ts +++ b/packages/@glimmer/runtime/lib/render.ts @@ -24,7 +24,7 @@ import { resolveComponent } from './component/resolve'; import { ARGS } from './symbols'; import { AotVM, InternalVM, JitVM } from './vm/append'; import { NewElementBuilder } from './vm/element-builder'; -import { DefaultDynamicScope } from './dynamic-scope'; +import { DefaultDynamicScope } from './scope'; import { UNDEFINED_REFERENCE } from './references'; class TemplateIteratorImpl implements TemplateIterator { diff --git a/packages/@glimmer/runtime/lib/scope.ts b/packages/@glimmer/runtime/lib/scope.ts new file mode 100644 index 0000000000..f1947756bd --- /dev/null +++ b/packages/@glimmer/runtime/lib/scope.ts @@ -0,0 +1,157 @@ +import { + Dict, + DynamicScope, + JitOrAotBlock, + PartialScope, + Scope, + ScopeBlock, + ScopeSlot, + Option, +} from '@glimmer/interfaces'; +import { PathReference, VersionedPathReference } from '@glimmer/reference'; +import { assign } from '@glimmer/util'; +import { UNDEFINED_REFERENCE } from './references'; + +export class DefaultDynamicScope implements DynamicScope { + private bucket: Dict; + + constructor(bucket?: Dict) { + if (bucket) { + this.bucket = assign({}, bucket); + } else { + this.bucket = {}; + } + } + + get(key: string): PathReference { + return this.bucket[key]; + } + + set(key: string, reference: PathReference): PathReference { + return (this.bucket[key] = reference); + } + + child(): DefaultDynamicScope { + return new DefaultDynamicScope(this.bucket); + } +} + +export function isScopeReference(s: ScopeSlot): s is VersionedPathReference { + if (s === null || Array.isArray(s)) return false; + return true; +} + +export class PartialScopeImpl implements PartialScope { + static root(self: PathReference, size = 0): PartialScope { + let refs: PathReference[] = new Array(size + 1); + + for (let i = 0; i <= size; i++) { + refs[i] = UNDEFINED_REFERENCE; + } + + return new PartialScopeImpl(refs, null, null, null).init({ self }); + } + + static sized(size = 0): Scope { + let refs: PathReference[] = new Array(size + 1); + + for (let i = 0; i <= size; i++) { + refs[i] = UNDEFINED_REFERENCE; + } + + return new PartialScopeImpl(refs, null, null, null); + } + + constructor( + // the 0th slot is `self` + readonly slots: Array>, + private callerScope: Option>, + // named arguments and blocks passed to a layout that uses eval + private evalScope: Option>>, + // locals in scope when the partial was invoked + private partialMap: Option>> + ) {} + + init({ self }: { self: PathReference }): this { + this.slots[0] = self; + return this; + } + + getSelf(): PathReference { + return this.get>(0); + } + + getSymbol(symbol: number): PathReference { + return this.get>(symbol); + } + + getBlock(symbol: number): Option> { + let block = this.get(symbol); + return block === UNDEFINED_REFERENCE ? null : (block as ScopeBlock); + } + + getEvalScope(): Option>> { + return this.evalScope; + } + + getPartialMap(): Option>> { + return this.partialMap; + } + + bind(symbol: number, value: ScopeSlot) { + this.set(symbol, value); + } + + bindSelf(self: PathReference) { + this.set>(0, self); + } + + bindSymbol(symbol: number, value: PathReference) { + this.set(symbol, value); + } + + bindBlock(symbol: number, value: Option>) { + this.set>>(symbol, value); + } + + bindEvalScope(map: Option>>) { + this.evalScope = map; + } + + bindPartialMap(map: Dict>) { + this.partialMap = map; + } + + bindCallerScope(scope: Option>): void { + this.callerScope = scope; + } + + getCallerScope(): Option> { + return this.callerScope; + } + + child(): Scope { + return new PartialScopeImpl( + this.slots.slice(), + this.callerScope, + this.evalScope, + this.partialMap + ); + } + + private get>(index: number): T { + if (index >= this.slots.length) { + throw new RangeError(`BUG: cannot get $${index} from scope; length=${this.slots.length}`); + } + + return this.slots[index] as T; + } + + private set>(index: number, value: T): void { + if (index >= this.slots.length) { + throw new RangeError(`BUG: cannot get $${index} from scope; length=${this.slots.length}`); + } + + this.slots[index] = value; + } +} diff --git a/packages/@glimmer/runtime/lib/vm/append.ts b/packages/@glimmer/runtime/lib/vm/append.ts index abda2cb7e0..eb767c9788 100644 --- a/packages/@glimmer/runtime/lib/vm/append.ts +++ b/packages/@glimmer/runtime/lib/vm/append.ts @@ -53,7 +53,7 @@ import { CheckNumber, check } from '@glimmer/debug'; import { unwrapHandle } from '@glimmer/util'; import { combineSlice } from '../utils/tags'; import { DidModifyOpcode, JumpIfNotModifiedOpcode, LabelOpcode } from '../compiled/opcodes/vm'; -import { ScopeImpl } from '../environment'; +import { PartialScopeImpl } from '../scope'; import { APPEND_OPCODES, DebugState, UpdatingOpcode } from '../opcodes'; import { UNDEFINED_REFERENCE } from '../references'; import { ARGS, CONSTANTS, DESTRUCTOR_STACK, HEAP, INNER_VM, REGISTERS, STACKS } from '../symbols'; @@ -488,7 +488,7 @@ export default abstract class VM implements PublicVM, I } pushRootScope(size: number): PartialScope { - let scope = ScopeImpl.sized(size); + let scope = PartialScopeImpl.sized(size); this[STACKS].scope.push(scope); return scope; } @@ -580,7 +580,7 @@ export default abstract class VM implements PublicVM, I function vmState( pc: number, - scope: Scope = ScopeImpl.root(UNDEFINED_REFERENCE, 0), + scope: Scope = PartialScopeImpl.root(UNDEFINED_REFERENCE, 0), dynamicScope: DynamicScope ) { return { @@ -610,7 +610,7 @@ export class AotVM extends VM implements InternalVM { runtime, vmState( runtime.program.heap.getaddr(handle), - ScopeImpl.root(UNDEFINED_REFERENCE, 0), + PartialScopeImpl.root(UNDEFINED_REFERENCE, 0), dynamicScope ), treeBuilder @@ -624,7 +624,7 @@ export class AotVM extends VM implements InternalVM { { handle, self, treeBuilder, dynamicScope }: InitOptions ) { let scopeSize = runtime.program.heap.scopesizeof(handle); - let scope = ScopeImpl.root(self, scopeSize); + let scope = PartialScopeImpl.root(self, scopeSize); let pc = check(runtime.program.heap.getaddr(handle), CheckNumber); let state = vmState(pc, scope, dynamicScope); let vm = initAOT(runtime, state, treeBuilder); @@ -666,7 +666,7 @@ export class JitVM extends VM implements InternalJitVM { { handle, self, dynamicScope, treeBuilder }: InitOptions ) { let scopeSize = runtime.program.heap.scopesizeof(handle); - let scope = ScopeImpl.root(self, scopeSize); + let scope = PartialScopeImpl.root(self, scopeSize); let state = vmState(runtime.program.heap.getaddr(handle), scope, dynamicScope); let vm = initJIT(context)(runtime, state, treeBuilder); vm.pushUpdating(); @@ -682,7 +682,7 @@ export class JitVM extends VM implements InternalJitVM { runtime, vmState( runtime.program.heap.getaddr(handle), - ScopeImpl.root(UNDEFINED_REFERENCE, 0), + PartialScopeImpl.root(UNDEFINED_REFERENCE, 0), dynamicScope ), treeBuilder