import { assert } from '@ember/debug'; import { combine, CONSTANT_TAG, UpdatableTag, } from '@glimmer/reference'; import { get, objectAt, tagFor, tagForProperty } from 'ember-metal'; import { _contentFor, isEmberArray } from 'ember-runtime'; import { guidFor, HAS_NATIVE_SYMBOL, isProxy } from 'ember-utils'; import { isEachIn } from '../helpers/each-in'; import { UpdatableReference } from './references'; const ITERATOR_KEY_GUID = 'be277757-bbbe-4620-9fcb-213ef433cca2'; export default function iterableFor(ref, keyPath) { if (isEachIn(ref)) { return new EachInIterable(ref, keyPath || '@key'); } else { return new EachIterable(ref, keyPath || '@identity'); } } class BoundedIterator { constructor(length, keyFor) { this.length = length; this.keyFor = keyFor; this.position = 0; } isEmpty() { return false; } memoFor(position) { return position; } next() { let { length, keyFor, position } = this; if (position >= length) { return null; } let value = this.valueFor(position); let memo = this.memoFor(position); let key = keyFor(value, memo, position); this.position++; return { key, value, memo }; } } class ArrayIterator extends BoundedIterator { constructor(array, length, keyFor) { super(length, keyFor); this.array = array; } static from(array, keyFor) { let { length } = array; if (length === 0) { return EMPTY_ITERATOR; } else { return new this(array, length, keyFor); } } static fromForEachable(object, keyFor) { let array = []; object.forEach(item => array.push(item)); return this.from(array, keyFor); } valueFor(position) { return this.array[position]; } } class EmberArrayIterator extends BoundedIterator { constructor(array, length, keyFor) { super(length, keyFor); this.array = array; } static from(array, keyFor) { let { length } = array; if (length === 0) { return EMPTY_ITERATOR; } else { return new this(array, length, keyFor); } } valueFor(position) { return objectAt(this.array, position); } } class ObjectIterator extends BoundedIterator { constructor(keys, values, length, keyFor) { super(length, keyFor); this.keys = keys; this.values = values; } static fromIndexable(obj, keyFor) { let keys = Object.keys(obj); let values = []; let { length } = keys; for (let i = 0; i < length; i++) { values.push(get(obj, keys[i])); } if (length === 0) { return EMPTY_ITERATOR; } else { return new this(keys, values, length, keyFor); } } static fromForEachable(obj, keyFor) { let keys = []; let values = []; let length = 0; let isMapLike = false; obj.forEach((value, key) => { isMapLike = isMapLike || arguments.length >= 2; if (isMapLike) { keys.push(key); } values.push(value); length++; }); if (length === 0) { return EMPTY_ITERATOR; } else if (isMapLike) { return new this(keys, values, length, keyFor); } else { return new ArrayIterator(values, length, keyFor); } } valueFor(position) { return this.values[position]; } memoFor(position) { return this.keys[position]; } } class NativeIterator { constructor(iterable, result, keyFor) { this.iterable = iterable; this.result = result; this.keyFor = keyFor; this.position = 0; } static from(iterable, keyFor) { let iterator = iterable[Symbol.iterator](); let result = iterator.next(); let { value, done } = result; if (done) { return EMPTY_ITERATOR; } else if (Array.isArray(value) && value.length === 2) { return new this(iterator, result, keyFor); } else { return new ArrayLikeNativeIterator(iterator, result, keyFor); } } isEmpty() { return false; } next() { let { iterable, result, position, keyFor } = this; if (result.done) { return null; } let value = this.valueFor(result, position); let memo = this.memoFor(result, position); let key = keyFor(value, memo, position); this.position++; this.result = iterable.next(); return { key, value, memo }; } } class ArrayLikeNativeIterator extends NativeIterator { valueFor(result) { return result.value; } memoFor(_result, position) { return position; } } class MapLikeNativeIterator extends NativeIterator { valueFor(result) { return result.value[1]; } memoFor(result) { return result.value[0]; } } const EMPTY_ITERATOR = { isEmpty() { return true; }, next() { assert('Cannot call next() on an empty iterator'); return null; }, }; class EachInIterable { constructor(ref, keyPath) { this.ref = ref; this.keyPath = keyPath; this.valueTag = UpdatableTag.create(CONSTANT_TAG); this.tag = combine([ref.tag, this.valueTag]); } iterate() { let { ref, valueTag } = this; let iterable = ref.value(); let tag = tagFor(iterable); if (isProxy(iterable)) { // this is because the each-in doesn't actually get(proxy, 'key') but bypasses it // and the proxy's tag is lazy updated on access iterable = _contentFor(iterable); } valueTag.inner.update(tag); if (!isIndexable(iterable)) { return EMPTY_ITERATOR; } if (Array.isArray(iterable) || isEmberArray(iterable)) { return ObjectIterator.fromIndexable(iterable, this.keyFor(true)); } else if (HAS_NATIVE_SYMBOL && isNativeIterable(iterable)) { return MapLikeNativeIterator.from(iterable, this.keyFor()); } else if (hasForEach(iterable)) { return ObjectIterator.fromForEachable(iterable, this.keyFor()); } else { return ObjectIterator.fromIndexable(iterable, this.keyFor(true)); } } valueReferenceFor(item) { return new UpdatableReference(item.value); } updateValueReference(ref, item) { ref.update(item.value); } memoReferenceFor(item) { return new UpdatableReference(item.memo); } updateMemoReference(ref, item) { ref.update(item.memo); } keyFor(hasUniqueKeys = false) { let { keyPath } = this; switch (keyPath) { case '@key': return hasUniqueKeys ? ObjectKey : Unique(MapKey); case '@index': return Index; case '@identity': return Unique(Identity); default: assert(`Invalid key: ${keyPath}`, keyPath[0] !== '@'); return Unique(KeyPath(keyPath)); } } } class EachIterable { constructor(ref, keyPath) { this.ref = ref; this.keyPath = keyPath; this.valueTag = UpdatableTag.create(CONSTANT_TAG); this.tag = combine([ref.tag, this.valueTag]); } iterate() { let { ref, valueTag } = this; let iterable = ref.value(); valueTag.inner.update(tagForProperty(iterable, '[]')); if (iterable === null || typeof iterable !== 'object') { return EMPTY_ITERATOR; } let keyFor = this.keyFor(); if (Array.isArray(iterable)) { return ArrayIterator.from(iterable, keyFor); } else if (isEmberArray(iterable)) { return EmberArrayIterator.from(iterable, keyFor); } else if (HAS_NATIVE_SYMBOL && isNativeIterable(iterable)) { return ArrayLikeNativeIterator.from(iterable, keyFor); } else if (hasForEach(iterable)) { return ArrayIterator.fromForEachable(iterable, keyFor); } else { return EMPTY_ITERATOR; } } valueReferenceFor(item) { return new UpdatableReference(item.value); } updateValueReference(ref, item) { ref.update(item.value); } memoReferenceFor(item) { return new UpdatableReference(item.memo); } updateMemoReference(ref, item) { ref.update(item.memo); } keyFor() { let { keyPath } = this; switch (keyPath) { case '@index': return Index; case '@identity': return Unique(Identity); default: assert(`Invalid key: ${keyPath}`, keyPath[0] !== '@'); return Unique(KeyPath(keyPath)); } } } function hasForEach(value) { return typeof value['forEach'] === 'function'; } function isNativeIterable(value) { return typeof value[Symbol.iterator] === 'function'; } function isIndexable(value) { return value !== null && (typeof value === 'object' || typeof value === 'function'); } // Position in an array is guarenteed to be unique function Index(_value, _memo, position) { return String(position); } // Object.keys(...) is guarenteed to be strings and unique function ObjectKey(_value, memo) { return memo; } // Map keys can be any objects function MapKey(_value, memo) { return Identity(memo); } function Identity(value) { switch (typeof value) { case 'string': return value; case 'number': return String(value); default: return guidFor(value); } } function KeyPath(keyPath) { return (value) => String(get(value, keyPath)); } function Unique(func) { let seen = {}; return (value, memo, position) => { let key = func(value, memo, position); let count = seen[key]; if (count === undefined) { seen[key] = 0; return key; } else { seen[key] = ++count; return `${key}${ITERATOR_KEY_GUID}${count}`; } }; }