22@module @ember /array
33*/
44import { DEBUG } from '@glimmer/env' ;
5- import { PROXY_CONTENT } from '@ember/-internals/metal' ;
5+ import { ComputedProperty , descriptorForDecorator , PROXY_CONTENT } from '@ember/-internals/metal' ;
66import { isEmberArray , setEmberArray } from '@ember/-internals/utils' ;
77import {
88 objectAt ,
@@ -1199,6 +1199,25 @@ interface EmberArray<T> extends Enumerable {
11991199 */
12001200 without ( value : T ) : NativeArray < T > ;
12011201}
1202+
1203+ const emberArrayBrackets = nonEnumerableComputed ( {
1204+ get ( ) {
1205+ return this ;
1206+ } ,
1207+ set ( _key , value ) {
1208+ this . replace ( 0 , this . length , value ) ;
1209+ return this ;
1210+ } ,
1211+ } ) ;
1212+
1213+ const emberArrayFirstObject = nonEnumerableComputed ( function ( ) {
1214+ return objectAt ( this , 0 ) ;
1215+ } ) . readOnly ( ) ;
1216+
1217+ const emberArrayLastObject = nonEnumerableComputed ( function ( ) {
1218+ return objectAt ( this , this . length - 1 ) ;
1219+ } ) . readOnly ( ) ;
1220+
12021221const EmberArray = Mixin . create ( Enumerable , {
12031222 init ( ) {
12041223 this . _super ( ...arguments ) ;
@@ -1209,23 +1228,11 @@ const EmberArray = Mixin.create(Enumerable, {
12091228 return indexes . map ( ( idx ) => objectAt ( this , idx ) ) ;
12101229 } ,
12111230
1212- '[]' : nonEnumerableComputed ( {
1213- get ( ) {
1214- return this ;
1215- } ,
1216- set ( _key , value ) {
1217- this . replace ( 0 , this . length , value ) ;
1218- return this ;
1219- } ,
1220- } ) ,
1231+ '[]' : emberArrayBrackets ,
12211232
1222- firstObject : nonEnumerableComputed ( function ( ) {
1223- return objectAt ( this , 0 ) ;
1224- } ) . readOnly ( ) ,
1233+ firstObject : emberArrayFirstObject ,
12251234
1226- lastObject : nonEnumerableComputed ( function ( ) {
1227- return objectAt ( this , this . length - 1 ) ;
1228- } ) . readOnly ( ) ,
1235+ lastObject : emberArrayLastObject ,
12291236
12301237 // Add any extra methods to EmberArray that are native to the built-in Array.
12311238 slice ( beginIndex = 0 , endIndex ?: number ) {
@@ -2099,6 +2106,127 @@ NativeArray.keys().forEach((methodName) => {
20992106
21002107NativeArray = NativeArray . without ( ...ignore ) ;
21012108
2109+ const NATIVE_ARRAY_METHODS = Object . freeze ( NativeArray . keys ( ) ) ;
2110+
2111+ function getCP ( decorator : Function ) {
2112+ let desc = descriptorForDecorator ( decorator ) ;
2113+ assert ( '[BUG] Expected descriptor with getter' , desc instanceof ComputedProperty ) ;
2114+ return desc ;
2115+ }
2116+
2117+ // NOTE: This code borrows heavily from https://github.com/tracked-tools/tracked-built-ins/blob/master/addon/src/-private/array.ts
2118+
2119+ // Unfortunately, TypeScript's ability to do inference *or* type-checking in a
2120+ // `Proxy`'s body is very limited, so we have to use a number of casts `as any`
2121+ // to make the internal accesses work. The type safety of these is guaranteed at
2122+ // the *call site* instead of within the body: you cannot do `Array.blah` in TS,
2123+ // and it will blow up in JS in exactly the same way, so it is safe to assume
2124+ // that properties within the getter have the correct type in TS.
2125+ class EmberAProxy < T = unknown > {
2126+ /**
2127+ * Creates an array from an iterable object.
2128+ * @param iterable An iterable object to convert to an array.
2129+ */
2130+ static from < T > ( iterable : Iterable < T > | ArrayLike < T > ) : EmberAProxy < T > {
2131+ return new EmberAProxy ( Array . from ( iterable ) ) ;
2132+ }
2133+
2134+ static of < T > ( ...arr : T [ ] ) : EmberAProxy < T > {
2135+ return new EmberAProxy ( arr ) ;
2136+ }
2137+
2138+ constructor ( arr : T [ ] = [ ] ) {
2139+ let self = this ;
2140+
2141+ let proxy = new Proxy ( arr , {
2142+ get ( target , prop /*, _receiver */ ) {
2143+ // These nonEnumerableComputed properties need special handling
2144+ if ( prop === '[]' ) {
2145+ return self . #bracketsCP. get ( proxy , prop ) ;
2146+ } else if ( prop === 'firstObject' ) {
2147+ return self . #firstObjectCP. get ( proxy , prop ) ;
2148+ } else if ( prop === 'lastObject' ) {
2149+ return self . #lastObjectCP. get ( proxy , prop ) ;
2150+ } else if (
2151+ typeof prop === 'string' &&
2152+ ( NATIVE_ARRAY_METHODS . has ( prop ) || prop === '_super' )
2153+ ) {
2154+ // If it's a NativeArray method, call it with the proxy as the target
2155+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2156+ return ( self as any ) [ prop ] . bind ( proxy ) ;
2157+ }
2158+
2159+ // Pass through every other property
2160+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2161+ return ( target as any ) [ prop ] ;
2162+ } ,
2163+
2164+ set ( target , prop , value /*, _receiver */ ) {
2165+ // These nonEnumerableComputed properties need special handling
2166+ if ( prop === '[]' ) {
2167+ self . #bracketsCP. set ( proxy , prop , value ) ;
2168+ } else if ( prop === 'firstObject' ) {
2169+ self . #firstObjectCP. set ( proxy , prop , value ) ;
2170+ } else if ( prop === 'lastObject' ) {
2171+ self . #lastObjectCP. set ( proxy , prop , value ) ;
2172+ } else {
2173+ // Pass through every other property
2174+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2175+ ( target as any ) [ prop ] = value ;
2176+ }
2177+
2178+ return true ;
2179+ } ,
2180+
2181+ getPrototypeOf ( ) {
2182+ return EmberAProxy . prototype ;
2183+ } ,
2184+ } ) as EmberAProxy < T > ;
2185+
2186+ // Call init so it's properly registered
2187+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2188+ ( self as any ) . init . call ( proxy ) ;
2189+
2190+ return proxy ;
2191+ }
2192+
2193+ #bracketsCP = getCP ( emberArrayBrackets ) ;
2194+ #firstObjectCP = getCP ( emberArrayFirstObject ) ;
2195+ #lastObjectCP = getCP ( emberArrayLastObject ) ;
2196+ }
2197+
2198+ // Apply NativeArray to the the EmberAProxy so that it appears as a NativeArray
2199+ // and so that the correct methods are available.
2200+ NativeArray . apply ( EmberAProxy . prototype ) ;
2201+
2202+ // This rule is correctly in the general case, but it doesn't understand
2203+ // declaration merging, which is how we're using the interface here. This
2204+ // declaration says that `EmberAProxy` acts just like `Array<T>`, but also has
2205+ // the properties declared via the `class` declaration above -- but without the
2206+ // cost of a subclass, which is much slower that the proxied array behavior.
2207+ // That is: a `EmberAProxy` *is* an `Array`, just with a proxy in front of
2208+ // accessors and setters, rather than a subclass of an `Array` which would be
2209+ // de-optimized by the browsers.
2210+ //
2211+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
2212+ interface EmberAProxy < T = unknown > extends NativeArray < T > { }
2213+
2214+ // Ensure instanceof works correctly
2215+ Object . setPrototypeOf ( EmberAProxy . prototype , Array . prototype ) ;
2216+
2217+ const nativeArrayThrowers = Object . fromEntries (
2218+ Array . from ( NativeArray . keys ( ) ) . map ( ( key ) => {
2219+ return [
2220+ key ,
2221+ ( ) =>
2222+ assert (
2223+ `Ember.A was incorrectly modifying the original array. Do not call the EmberArray function ${ key } on the original array. Instead, call it on the value returned by Ember.A.`
2224+ ) ,
2225+ ] ;
2226+ } )
2227+ ) ;
2228+ const ModifiedNativeArray = Mixin . create ( nativeArrayThrowers ) ;
2229+
21022230let A : < T > ( arr ?: Array < T > ) => NativeArray < T > ;
21032231
21042232if ( ENV . EXTEND_PROTOTYPES . Array ) {
@@ -2124,8 +2252,13 @@ if (ENV.EXTEND_PROTOTYPES.Array) {
21242252 // SAFETY: If it's a true native array and it is also an EmberArray then it should be an Ember NativeArray
21252253 return arr as unknown as NativeArray < T > ;
21262254 } else {
2127- // SAFETY: This will return an NativeArray but TS can't infer that.
2128- return NativeArray . apply ( arr ?? [ ] ) as NativeArray < T > ;
2255+ // Remove this in Ember 5.0.
2256+ if ( DEBUG ) {
2257+ if ( arr ) {
2258+ ModifiedNativeArray . apply ( arr ) ;
2259+ }
2260+ }
2261+ return new EmberAProxy ( arr ?? [ ] ) ;
21292262 }
21302263 } ;
21312264}
0 commit comments