import { areEqual, isArray, isPromise, isMap, isSet, isNumber, type IDisposable, onResolve, type IIndexable, resolve, all, emptyArray, type IContainer, } from '@aurelia/kernel'; import { BindingBehaviorExpression, ForOfStatement, type IsBindingBehavior, ValueConverterExpression, } from '@aurelia/expression-parser'; import { type Collection, CollectionObserver, getCollectionObserver, type IndexMap, createIndexMap, astEvaluate, astAssign, Scope, BindingContext, type IOverrideContext, } from '@aurelia/runtime'; import { IExpressionParser } from '@aurelia/expression-parser'; import { IRenderLocation } from '../../dom'; import { IPlatform } from '../../platform'; import { IViewFactory } from '../../templating/view'; import { isSSRTemplateController, adoptSSRViews, type ISSRTemplateController } from '../../templating/ssr'; import { CustomAttributeStaticAuDefinition, attrTypeName } from '../custom-attribute'; import { IController } from '../../templating/controller'; import { rethrow, etIsProperty } from '../../utilities'; import { HydrateTemplateController, IInstruction, IteratorBindingInstruction } from '@aurelia/template-compiler'; import type { PropertyBinding } from '../../binding/property-binding'; import type { ISyntheticView, ICustomAttributeController, IHydratableController, ICustomAttributeViewModel, IHydratedController, IHydratedParentController, ControllerVisitor } from '../../templating/controller'; import { ErrorNames, createMappedError } from '../../errors'; import { createInterface, singletonRegistration } from '../../utilities-di'; type Items = C | undefined; function dispose(disposable: IDisposable): void { disposable.dispose(); } const wrappedExprs = [ 'BindingBehavior', 'ValueConverter', ]; export class Repeat implements ICustomAttributeViewModel { public static readonly $au: CustomAttributeStaticAuDefinition = { type: attrTypeName, name: 'repeat', isTemplateController: true, defaultProperty: 'items', bindables: ['items'], }; public views: ISyntheticView[] = []; public forOf!: ForOfStatement; public local!: string; public readonly $controller!: ICustomAttributeController; // This is set by the controller after this instance is constructed public items: Items; public key: null | string | IsBindingBehavior = null; public contextual: boolean = true; /** @internal */ private _oldViews: ISyntheticView[] = []; /** @internal */ private _scopes: Scope[] = []; /** @internal */ private _oldScopes: Scope[] = []; /** @internal */ private _scopeMap: Map = new Map(); /** @internal */ private _observer?: CollectionObserver = void 0; /** @internal */ private _innerItems: Items | null; /** @internal */ private _forOfBinding!: PropertyBinding; /** @internal */ private _observingInnerItems: boolean = false; /** @internal */ private _reevaluating: boolean = false; /** @internal */ private _innerItemsExpression: IsBindingBehavior | null = null; /** @internal */ private _normalizedItems?: unknown[] = void 0; /** @internal */ private _hasDestructuredLocal: boolean = false; /** @internal */ private readonly _contextualExpr?: IsBindingBehavior; /** @internal */ private _hasAdoptedViews: boolean = false; /** @internal */ private readonly _location = resolve(IRenderLocation); /** @internal */ private readonly _parent = resolve(IController) as IHydratableController; /** @internal */ private readonly _factory = resolve(IViewFactory); /** @internal */ private readonly _resolver = resolve(IRepeatableHandlerResolver); /** @internal */ private readonly _platform = resolve(IPlatform); public constructor() { const instruction = resolve(IInstruction) as HydrateTemplateController; const iteratorProps = (instruction.props[0] as IteratorBindingInstruction).props; for (let i = 0, ii = iteratorProps.length; i < ii; ++i) { const prop = iteratorProps[i]; const { to, value, command } = prop; if (to === 'key') { if (command === null) { this.key = value as string; } else if (command === 'bind') { // AOT: value is pre-parsed AST; JIT: value is string to parse this.key = typeof value === 'string' ? resolve(IExpressionParser).parse(value, etIsProperty) : value; } else { throw createMappedError(ErrorNames.repeat_invalid_key_binding_command, command); } } else if (to === 'contextual') { if (command === null) { // Static value: contextual: true | false // When command is null, value is always a string this.contextual = value === 'false' ? false : !!value; } else if (command === 'bind') { // Expression: contextual.bind: someExpression (evaluated once at bind) // AOT: value is pre-parsed AST; JIT: value is string to parse this._contextualExpr = typeof value === 'string' ? resolve(IExpressionParser).parse(value, etIsProperty) : value; } else { throw createMappedError(ErrorNames.repeat_invalid_contextual_binding_command, command); } } else { throw createMappedError(ErrorNames.repeat_extraneous_binding, to); } } } public binding( _initiator: IHydratedController, _parent: IHydratedParentController, ): void | Promise { const bindings = this._parent.bindings as PropertyBinding[]; const ii = bindings.length; let binding: PropertyBinding = (void 0)!; let forOf!: ForOfStatement; let i = 0; for (; ii > i; ++i) { binding = bindings[i]; if (binding.target === this && binding.targetProperty === 'items') { forOf = this.forOf = binding.ast as ForOfStatement; this._forOfBinding = binding; let expression = forOf.iterable; while (expression != null && wrappedExprs.includes(expression.$kind)) { expression = (expression as ValueConverterExpression | BindingBehaviorExpression).expression; this._observingInnerItems = true; } this._innerItemsExpression = expression; break; } } this._refreshCollectionObserver(); const dec = forOf.declaration; if(!(this._hasDestructuredLocal = dec.$kind === 'ArrayDestructuring' || dec.$kind === 'ObjectDestructuring')) { this.local = astEvaluate(dec, this.$controller.scope, binding, null) as string; } // Evaluate contextual.bind expression if present (one-time evaluation at bind) if (this._contextualExpr !== void 0) { const result = astEvaluate(this._contextualExpr, this.$controller.scope, binding, null); this.contextual = result != null && result !== false; } } public attaching( initiator: IHydratedController, _parent: IHydratedParentController, ): void | Promise { this._normalizeToArray(); this._createScopes(void 0); return this._activateAllViews(initiator, this._normalizedItems ?? emptyArray); } public detaching( initiator: IHydratedController, _parent: IHydratedParentController, ): void | Promise { this._refreshCollectionObserver(); // Adopted views can't be cached - their nodes are tied to specific DOM const skipCache = this._hasAdoptedViews; this._hasAdoptedViews = false; const result = this._deactivateAllViews(initiator, skipCache); if (skipCache) { this.views = []; } return result; } public unbinding( _initiator: IHydratedController, _parent: IHydratedParentController, ): void | Promise { this._scopeMap.clear(); } // called by SetterObserver public itemsChanged(): void { if (!this.$controller.isActive) { return; } this._refreshCollectionObserver(); this._normalizeToArray(); this._createScopes(void 0); this._applyIndexMap(void 0); } public handleCollectionChange(collection: Collection, indexMap: IndexMap | undefined): void { const $controller = this.$controller; if (!$controller.isActive) { return; } if (this._observingInnerItems) { if (this._reevaluating) { return; } this._reevaluating = true; this.items = astEvaluate(this.forOf.iterable, $controller.scope, this._forOfBinding, null) as Items; this._reevaluating = false; return; } this._normalizeToArray(); this._createScopes(this.key === null ? indexMap : void 0); this._applyIndexMap(indexMap); } /** @internal */ private _applyIndexMap(indexMap: IndexMap | undefined): void { const oldViews = this.views; this._oldViews = oldViews.slice(); const oldLen = oldViews.length; const hasKey = this.key !== null; const oldScopes = this._oldScopes; const newScopes = this._scopes; if (hasKey || indexMap === void 0) { const local = this.local; const dec = this.forOf.declaration; const binding = this._forOfBinding; const hasDestructuredLocal = this._hasDestructuredLocal; const newLen = newScopes.length; indexMap = createIndexMap(newLen); if (oldLen === 0) { // Only add new views for (let i = 0; i < newLen; ++i) { indexMap[i] = -2; } } else if (newLen === 0) { // Only remove old views for (let i = 0; i < oldLen; ++i) { indexMap.deletedIndices.push(i); indexMap.deletedItems.push(getItem(hasDestructuredLocal, dec, oldScopes[i], binding, local)); } } else { // O(n) matching via scope identity. // _createScopes already matched by key (or item identity), reusing old Scope objects. // So newScopes[i] === oldScopes[j] iff the key/item at new position i came from old position j. const oldScopeToIndex = new Map(); for (let i = 0; i < oldLen; ++i) { oldScopeToIndex.set(oldScopes[i], i); } const usedOldIndices = new Set(); for (let i = 0; i < newLen; ++i) { const oldIdx = oldScopeToIndex.get(newScopes[i]); if (oldIdx !== void 0) { indexMap[i] = oldIdx; usedOldIndices.add(oldIdx); } else { indexMap[i] = -2; } } // Collect deletions in ascending order for (let i = 0; i < oldLen; ++i) { if (!usedOldIndices.has(i)) { indexMap.deletedIndices.push(i); indexMap.deletedItems.push(getItem(hasDestructuredLocal, dec, oldScopes[i], binding, local)); } } } } // first detach+unbind+(remove from array) the deleted view indices if (indexMap.deletedIndices.length > 0) { const ret = onResolve( this._deactivateAndRemoveViewsByKey(indexMap), () => { // TODO(fkleuver): add logic to the controller that ensures correct handling of race conditions and add a variety of `if` integration tests return this._createAndActivateAndSortViewsByKey(indexMap); }, ); if (isPromise(ret)) { ret.catch(rethrow); } } else { // TODO(fkleuver): add logic to the controller that ensures correct handling of race conditions and add integration tests // eslint-disable-next-line @typescript-eslint/no-floating-promises this._createAndActivateAndSortViewsByKey(indexMap); } } // todo: subscribe to collection from inner expression /** @internal */ private _refreshCollectionObserver(): void { const scope = this.$controller.scope; let innerItems = this._innerItems; let observingInnerItems = this._observingInnerItems; let newObserver: CollectionObserver | undefined; if (observingInnerItems) { innerItems = this._innerItems = astEvaluate(this._innerItemsExpression!, scope, this._forOfBinding, null) as Items ?? null; observingInnerItems = this._observingInnerItems = !areEqual(this.items, innerItems); } const oldObserver = this._observer; if (this.$controller.isActive) { const items = observingInnerItems ? innerItems : this.items; newObserver = this._observer = this._resolver.resolve(items).getObserver?.(items); if (oldObserver !== newObserver) { oldObserver?.unsubscribe(this); newObserver?.subscribe(this); } } else { oldObserver?.unsubscribe(this); this._observer = undefined; } } /** @internal */ private _createScopes(indexMap: IndexMap | undefined): void { const oldScopes = this._scopes; this._oldScopes = oldScopes.slice(); const items = this._normalizedItems!; const len = items.length; const scopes = this._scopes = Array(len); const oldScopeMap = this._scopeMap; const newScopeMap = new Map(); const parentScope = this.$controller.scope; const binding = this._forOfBinding; const forOf = this.forOf; const local = this.local; const hasDestructuredLocal = this._hasDestructuredLocal; if (indexMap === void 0) { const key = this.key; const hasKey = key !== null; if (hasKey) { const keys = Array(len); if (typeof key === 'string') { for (let i = 0; i < len; ++i) { keys[i] = (items[i] as IIndexable)[key]; } } else { for (let i = 0; i < len; ++i) { // This method of creating a throwaway scope just for key evaluation is inefficient but requires a lot less code this way. // It seems acceptable for what should be a niche use case and this way it's guaranteed to work correctly in all cases. // When performance matters, it is advised to use normal string-based keys instead of expressions: // `repeat.for="i of items; key.bind: i.key" - inefficient // `repeat.for="i of items; key: key" - efficient const scope = createScope(items[i], forOf, parentScope, binding, local, hasDestructuredLocal); setItem(hasDestructuredLocal, forOf.declaration, scope, binding, local, items[i]); keys[i] = astEvaluate(key, scope, binding, null); } } for (let i = 0; i < len; ++i) { scopes[i] = getScope(oldScopeMap, newScopeMap, keys[i], items[i], forOf, parentScope, binding, local, hasDestructuredLocal); } } else { for (let i = 0; i < len; ++i) { scopes[i] = getScope(oldScopeMap, newScopeMap, items[i], items[i], forOf, parentScope, binding, local, hasDestructuredLocal); } } } else { const oldLen = oldScopes.length; for (let i = 0; i < len; ++i) { const src = indexMap[i]; if (src >= 0 && src < oldLen) { scopes[i] = oldScopes[src]; } else { scopes[i] = createScope(items[i], forOf, parentScope, binding, local, hasDestructuredLocal); } setItem(hasDestructuredLocal, forOf.declaration, scopes[i], binding, local, items[i]); } } oldScopeMap.clear(); this._scopeMap = newScopeMap; } /** @internal */ private _normalizeToArray(): void { const items = this.items; if (isArray(items)) { this._normalizedItems = items.slice(0); return; } const normalizedItems: unknown[] = []; this._resolver.resolve(items).iterate(items, (item, index) => { normalizedItems[index] = item; }); this._normalizedItems = normalizedItems; } /** @internal */ private _activateAllViews( initiator: IHydratedController | null, $items: unknown[], ): void | Promise { // SSR hydration: adopt existing DOM instead of creating new views. // _hydrateViews clears ssrScope, so reactivation takes the normal path. const ssrScope = this.$controller.ssrScope; if (ssrScope != null && isSSRTemplateController(ssrScope) && ssrScope.type === 'repeat') { return this._hydrateViews(initiator, $items, ssrScope); } return this._activateAllViewsFresh(initiator, $items); } /** @internal SSR hydration: adopt existing DOM nodes instead of creating new ones. */ private _hydrateViews( initiator: IHydratedController | null, $items: unknown[], ssrScope: ISSRTemplateController, ): void | Promise { const { $controller, _factory, _location, _scopes, _platform } = this; const newLen = $items.length; const { views: adoptedViews } = adoptSSRViews(ssrScope, _factory, $controller, _location, _platform); if (adoptedViews.length === 0) { $controller.ssrScope = undefined; return this._activateAllViewsFresh(initiator, $items); } this._hasAdoptedViews = true; this.views = adoptedViews; let promises: Promise[] | undefined = void 0; for (let i = 0; i < newLen; ++i) { const view = adoptedViews[i]; const scope = _scopes[i]; if (this.contextual) { setContextualProperties(scope.overrideContext as RepeatOverrideContext, i, newLen, $items); } const ret = view.activate(initiator ?? view, $controller, scope); if (isPromise(ret)) { (promises ??= []).push(ret); } } $controller.ssrScope = undefined; if (promises !== void 0) { return promises.length === 1 ? promises[0] : Promise.all(promises) as unknown as Promise; } } /** @internal */ private _activateAllViewsFresh( initiator: IHydratedController | null, $items: unknown[], ): void | Promise { const { $controller, _factory, _location, _scopes } = this; const newLen = $items.length; const views = this.views = Array(newLen); let promises: Promise[] | undefined = void 0; for (let i = 0; i < newLen; ++i) { const view = views[i] = _factory.create($controller).setLocation(_location); view.nodes.unlink(); const scope = _scopes[i]; if (this.contextual) { setContextualProperties(scope.overrideContext as RepeatOverrideContext, i, newLen, $items); } const ret = view.activate(initiator ?? view, $controller, scope); if (isPromise(ret)) { (promises ??= []).push(ret); } } if (promises !== void 0) { return promises.length === 1 ? promises[0] : Promise.all(promises) as unknown as Promise; } } /** @internal */ private _deactivateAllViews( initiator: IHydratedController | null, skipCache: boolean = false, ): void | Promise { let promises: Promise[] | undefined = void 0; let ret: void | Promise; let view: ISyntheticView; let i = 0; const { views, $controller } = this; const ii = views.length; for (; ii > i; ++i) { view = views[i]; // Adopted views can't be reused if (!skipCache) { view.release(); } ret = view.deactivate(initiator ?? view, $controller); if (isPromise(ret)) { (promises ?? (promises = [])).push(ret); } } if (promises !== void 0) { return (promises.length === 1 ? promises[0] : Promise.all(promises)) as unknown as Promise; } } /** @internal */ private _deactivateAndRemoveViewsByKey( indexMap: IndexMap, ): void | Promise { let promises: Promise[] | undefined = void 0; let ret: void | Promise; let view: ISyntheticView; const { $controller, views } = this; const deleted = indexMap.deletedIndices.slice().sort(compareNumber); const deletedLen = deleted.length; let i = 0; for (; deletedLen > i; ++i) { view = views[deleted[i]]; view.release(); ret = view.deactivate(view, $controller); if (isPromise(ret)) { (promises ?? (promises = [])).push(ret); } } i = 0; for (; deletedLen > i; ++i) { views.splice(deleted[i] - i, 1); } if (promises !== void 0) { return promises.length === 1 ? promises[0] : Promise.all(promises) as unknown as Promise; } } /** @internal */ private _createAndActivateAndSortViewsByKey( indexMap: IndexMap, ): void | Promise { let promises: Promise[] | undefined = void 0; let ret: void | Promise; let view: ISyntheticView; let i = 0; const { $controller, _factory, _location, views, _scopes, _oldViews } = this; const newLen = indexMap.length; for (; newLen > i; ++i) { if (indexMap[i] === -2) { view = _factory.create($controller); views.splice(i, 0, view); } } if (views.length !== newLen) { throw createMappedError(ErrorNames.repeat_mismatch_length, [views.length, newLen]); } let source = 0; i = 0; for (; i < indexMap.length; ++i) { if ((source = indexMap[i]) !== -2) { views[i] = _oldViews[source]; } } // this algorithm retrieves the indices of the longest increasing subsequence of items in the repeater // the items on those indices are not moved; this minimizes the number of DOM operations that need to be performed const seq = longestIncreasingSubsequence(indexMap); const seqLen = seq.length; let next: ISyntheticView; let j = seqLen - 1; i = newLen - 1; for (; i >= 0; --i) { view = views[i]; next = views[i + 1]; if (this.contextual) { setContextualProperties(_scopes[i].overrideContext as RepeatOverrideContext, i, newLen, this._normalizedItems); } if (indexMap[i] === -2) { view.nodes.link(next?.nodes ?? _location); view.setLocation(_location); ret = view.activate(view, $controller, _scopes[i]); if (isPromise(ret)) { (promises ?? (promises = [])).push(ret); } } else if (j < 0 || i !== seq[j]) { view.nodes.link(next?.nodes ?? _location); view.nodes.insertBefore(view.location!); } else { --j; } } if (promises !== void 0) { return promises.length === 1 ? promises[0] : Promise.all(promises) as unknown as Promise; } } public dispose(): void { this.views.forEach(dispose); this.views = (void 0)!; } public accept(visitor: ControllerVisitor): void | true { const { views } = this; if (views !== void 0) { for (let i = 0, ii = views.length; i < ii; ++i) { const result = views[i].accept(visitor); if (result === true) { return true; } } } } } let maxLen = 16; let prevIndices = new Int32Array(maxLen); let tailIndices = new Int32Array(maxLen); // Based on inferno's lis_algorithm @ https://github.com/infernojs/inferno/blob/master/packages/inferno/src/DOM/patching.ts#L732 // with some tweaks to make it just a bit faster + account for IndexMap (and some names changes for readability) /** @internal */ export function longestIncreasingSubsequence(indexMap: IndexMap): Int32Array { const len = indexMap.length; if (len > maxLen) { maxLen = len; prevIndices = new Int32Array(len); tailIndices = new Int32Array(len); } let cursor = 0; let cur = 0; let prev = 0; let i = 0; let j = 0; let low = 0; let high = 0; let mid = 0; for (; i < len; i++) { cur = indexMap[i]; if (cur !== -2) { j = prevIndices[cursor]; prev = indexMap[j]; if (prev !== -2 && prev < cur) { tailIndices[i] = j; prevIndices[++cursor] = i; continue; } low = 0; high = cursor; while (low < high) { mid = (low + high) >> 1; prev = indexMap[prevIndices[mid]]; if (prev !== -2 && prev < cur) { low = mid + 1; } else { high = mid; } } prev = indexMap[prevIndices[low]]; if (cur < prev || prev === -2) { if (low > 0) { tailIndices[i] = prevIndices[low - 1]; } prevIndices[low] = i; } } } i = ++cursor; const result = new Int32Array(i); cur = prevIndices[cursor - 1]; while (cursor-- > 0) { result[cursor] = cur; cur = tailIndices[cur]; } while (i-- > 0) prevIndices[i] = 0; return result; } interface IRepeatOverrideContext extends IOverrideContext { $index: number; $odd: boolean; $even: boolean; $first: boolean; $middle: boolean; $last: boolean; $length: number; // new in v2, there are a few requests, not sure if it should stay __items__?: unknown[]; // opt-in: the array being iterated (undefined when disabled) $previous?: unknown; // opt-in: previous iteration's item (null for first, undefined when disabled) } class RepeatOverrideContext implements IRepeatOverrideContext { public get $odd(): boolean { return !this.$even; } public get $even(): boolean { return this.$index % 2 === 0; } public get $first(): boolean { return this.$index === 0; } public get $middle(): boolean { return !this.$first && !this.$last; } public get $last(): boolean { return this.$index === this.$length - 1; } public get $previous(): unknown { return this.__items__?.[this.$index - 1]; } public constructor( public readonly $index: number = 0, public readonly $length: number = 1, // maybe at some point we can turn this into $items // to indicate a normalised array of any collection public readonly __items__: unknown[] | undefined = undefined, ) {} } const setContextualProperties = (oc: IRepeatOverrideContext, index: number, length: number, items: unknown[] | undefined): void => { oc.$index = index; oc.$length = length; oc.__items__ = items; }; export const IRepeatableHandlerResolver = /*@__PURE__*/ createInterface( 'IRepeatableHandlerResolver', x => x.singleton(RepeatableHandlerResolver) ); /** * An interface describings the capabilities of a repeatable handler. */ export interface IRepeatableHandlerResolver { resolve(value: unknown): IRepeatableHandler; } /** * The default implementation of the IRepeatableHandlerResolver interface */ class RepeatableHandlerResolver implements IRepeatableHandlerResolver { /** @internal */ private readonly _handlers = resolve(all(IRepeatableHandler)); public resolve(value: Repeatable): IRepeatableHandler { if (_arrayHandler.handles(value)) { return _arrayHandler; } if (_setHandler.handles(value)) { return _setHandler; } if (_mapHandler.handles(value)) { return _mapHandler; } if (_numberHandler.handles(value)) { return _numberHandler; } if (_nullishHandler.handles(value)) { return _nullishHandler; } const handler = this._handlers.find(x => x.handles(value)); if (handler !== void 0) { return handler; } return _unknownHandler; } } /** * A simple implementation for handling common array like values, such as: * - HTMLCollection * - NodeList * - FileList, * - etc... */ export class ArrayLikeHandler implements IRepeatableHandler> { public static register(c: IContainer) { c.register(singletonRegistration(IRepeatableHandler, this)); } public handles(value: NonNullable): boolean { return 'length' in value && isNumber(value.length); } public iterate(items: ArrayLike, func: (item: unknown, index: number, arr: ArrayLike) => void): void { for (let i = 0, ii = items.length; i < ii; ++i) { func(items[i], i, items); } } } /** * An interface describing a repeatable value handler */ export const IRepeatableHandler = /*@__PURE__*/ createInterface('IRepeatableHandler'); export interface IRepeatableHandler { handles(value: unknown): boolean; getObserver?(value: TValue): CollectionObserver | undefined; iterate(value: TValue, func: (item: unknown, index: number, value: TValue) => void): void; // getCount(items: TValue): number; } const _arrayHandler: IRepeatableHandler = { handles: isArray, getObserver: getCollectionObserver, /* istanbul ignore next */ iterate(value, func): void { const ii = value.length; let i = 0; for (; i < ii; ++i) { func(value[i], i, value); } }, // getCount: items => items.length, }; const _setHandler: IRepeatableHandler> = { handles: isSet, getObserver: getCollectionObserver, iterate(value, func): void { let i = 0; let key: unknown; for (key of value.keys()) { func(key, i++, value); } }, // getCount: s => s.size, }; const _mapHandler: IRepeatableHandler> = { handles: isMap, getObserver: getCollectionObserver, iterate(value, func): void { let i = 0; let entry: [unknown, unknown] | undefined; for (entry of value.entries()) { func(entry, i++, value); } }, // getCount: s => s.size, }; const _numberHandler: IRepeatableHandler = { handles: isNumber, iterate(value, func): void { let i = 0; for (; i < value; ++i) { func(i, i, value); } }, // getCount: v => v, }; const _nullishHandler: IRepeatableHandler = { handles: v => v == null, iterate() {/* do nothing */}, // getCount: () => 0, }; const _unknownHandler: IRepeatableHandler = { handles(_value: unknown): boolean { // Should only return as an explicit last fallback return false; }, iterate(value: Repeatable, _func: (item: unknown, index: number, value: Repeatable) => void): void { throw createMappedError(ErrorNames.repeat_non_iterable, value); }, // getCount: () => 0, }; type Repeatable = Collection | ArrayLike | number | null | undefined; const setItem = ( hasDestructuredLocal: boolean, dec: ForOfStatement['declaration'], scope: Scope, binding: PropertyBinding, local: string, item: unknown, ) => { if (hasDestructuredLocal) { astAssign(dec, scope, binding, null, item); } else { scope.bindingContext[local] = item; } }; const getItem = ( hasDestructuredLocal: boolean, dec: ForOfStatement['declaration'], scope: Scope, binding: PropertyBinding, local: string, ): unknown => { return hasDestructuredLocal ? astEvaluate(dec, scope, binding, null) : scope.bindingContext[local]; }; const getScope = ( oldScopeMap: Map, newScopeMap: Map, key: unknown, item: unknown, forOf: ForOfStatement, parentScope: Scope, binding: PropertyBinding, local: string, hasDestructuredLocal: boolean, ) => { let scope = oldScopeMap.get(key); if (scope === void 0) { scope = createScope(item, forOf, parentScope, binding, local, hasDestructuredLocal); } else if (scope instanceof Scope) { oldScopeMap.delete(key); } else if (scope.length === 1) { scope = scope[0]; oldScopeMap.delete(key); } else { scope = scope.shift()!; } if (newScopeMap.has(key)) { const entry = newScopeMap.get(key)!; if (entry instanceof Scope) { newScopeMap.set(key, [entry, scope]); } else { entry.push(scope); } } else { newScopeMap.set(key, scope); } setItem(hasDestructuredLocal, forOf.declaration, scope, binding, local, item); return scope; }; const createScope = ( item: unknown, forOf: ForOfStatement, parentScope: Scope, binding: PropertyBinding, local: string, hasDestructuredLocal: boolean, ) => { if (hasDestructuredLocal) { const scope = Scope.fromParent(parentScope, new BindingContext(), new RepeatOverrideContext()); astAssign(forOf.declaration, scope, binding, null, item); return scope; } return Scope.fromParent(parentScope, new BindingContext(local, item), new RepeatOverrideContext()); }; const compareNumber = (a: number, b: number): number => a - b;