Brace expension in computed with unknownProperty

Hi, I recently refactored a service from a PromiseProxyMixin to an Octane Service. This new service use unknownProperty to replicate the previous behaviour :

this.myService.someRandomProperty

class myService extends Service {
	#get(property) {
		if (this.data) {
			return this.data[property];
		}
		else {
			return this.myPromise.then(() => {
				return this.data[property];
			});
		}
	}

	unknownProperty(key) {
		return this.#get(key);
	}
}

(simplified snippet)

That part works perfectly.

I have problem when brace expension is used in @computed like so :

@computed('myService.{prop1,prop2,prop3}')
get myGetter() {
}

In this case the computed does not works as attended i have to get the “data” propertie of the service like so

@computed('myService.data.{prop1,prop2,prop3}')

Is this a known limitation of unknownProperty ? What happen behind the scene when brace expansion is used ? If i can i 'd like to avoid having to use my service differently in computed

Note : We are running ember 3.24 et slowly migrating to full octane code and more recent ember version , but for now we still have to deal with some computed.

Thanks :slight_smile:

First, unknownProperty is a feature available to pre-octane ember. Granted it still likely works in later versions it is a big foot-gun and I’m pretty sure I seen talks about removing it.

Good news is that since we now focus on @tracked in native classes this kind of thing can be achieved with native JavaScript’s Proxy—with a little redesign.

To prepare yourself for that eventual upgrade I might approach this by splitting the responsibilities up. One of the advantage of the now deprecated PromiseProxyMixin is that it had a consistent type. It didn’t matter if it proxy’ed a Promise or a raw value it always offered the same interface (.content, .isPending, .isFulfilled, .isRejected, and .reason). In more modern Ember we would either make our own wrapper for this or use an addon like ember-concurrency.

If this were octane and I did not have ember-concurrency I think I would consider faking updates by using a counter system. The down side to this is that reactivity is at the top most level. I’d have to pull-in another addon to facilitate deep reactivity.

Based on the code above It looks like the reactive aspects is likely just the promise resolution. Using that assumption we can probably simplify things by moving the hard work to another class and offering a Proxy to consumers of the service. It does introduce one level of properties but that does keep things clean and less magic going on plus avoid the confusing unknownProperty mechanics.

class AsyncThing {
  status = 'pending';

  get isPending() { return this.status === 'pending'; }
  get isFulfilled() { return this.status === 'fulfilled'; }
  get isRejected() { return this.status === 'rejected'; }

  constructor(maybePromise) {
    this.promise = Promise.resolve(maybePromise);
    this.promise.then(
      (value) => {
        this.status = 'fulfilled';
        this.value = value;
      },
      (reason) => {
        this.status = 'rejected';
        this.reason = reason;
      },
    );
  }
}

export default class MyService extends Service {
  #cache = new Map();

  data = new Proxy({}, {
    get: (_, prop) => this.#cache.get(prop) ?? this[prop],
    set: (_, prop, value) => {
      const notifyChanged = () => this.notifyPropertyChanged(`data.${prop}`);
      const wrapper = new AsyncThing(value);
      wrapper.promise.finally(notifyChanged);
      this.#cache.set(prop, wrapper);
      notifyChanged();
      return true;
    },
  });
}

By contrast in Octane it would look more like this:

class AsyncThing {
  @tracked value;
  @tracked reason;
  @tracked status = 'pending';

  get isPending() { return this.status === 'pending'; }
  get isFulfilled() { return this.status === 'fulfilled'; }
  get isRejected() { return this.status === 'rejected'; }

  constructor(maybePromise) {
    this.promise = Promise.resolve(maybePromise);
    this.promise.then(
      (value) => {
        this.status = 'fulfilled';
        this.value = value;
      },
      (reason) => {
        this.status = 'rejected';
        this.reason = reason;
      },
    );
  }
}

export default class MyService extends Service {
  @tracked _counter = 0;
  #cache = new Map();

  data = new Proxy({}, {
    get: (_, prop) => {
      this._counter;
      return this.#cache.get(prop) ?? this[prop];
    },
    set: (_, prop, value) => {
      this.#cache.set(prop, new AsyncThing(value));
      this._counter++;
      return true;
    },
  });
}

Not sure how useful this all is. It isn’t an exact answer but it does offer an opportunity to rethink you “Data Down; Actions Up” design.

Also, I haven’t used this addon in years but it might offer something for you? ember-tracked-polyfill

Whoa whoa wait! @tracked was available in 3.24!

Use the @tracked version above. Better yet consider managing this with ember-concurrency and/or tracked-built-ins

I made that previous post way too complicated!

Also, once you get past 3.26 you’ll get access to ember-resources which would simplify all of this into almost a one liner. ember-resources is awesome and makes this so much easier.

@NullVoxPopuli mind checking my accuracy on this thread?

ofc :tada:

tracked is available in 3.13+

yeah, minimum possible working version is 3.25, but 3.28 is preferred.

thanks!! <3

it’s still shallow reactivity, but allowing dynamic reactive entries within the flat structures (Object, Array, Map, Set, WeakMap, WeakSet). For deep reactivity, there is

I’m against counters being exposed as public API. Instead, if you’re wanting to just “retry” a failed a promise / function, I’d implement a retry method that calls the async-function again (but maybe I’m missing something – not sure what the purpose is exactly in my skimming).

aka “one way data flow”

hope this helps!