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