data icon indicating copy to clipboard operation
data copied to clipboard

The Road to PolarisMode

Open runspired opened this issue 3 months ago • 2 comments

Three things stand between us and marking PolarisMode as recommended.

  1. expose reactive state props which will be added by withDefaults
  2. expose utilities for common operations like rollback (might be the only one)
  3. ship a story for relationships.

Of these, relationships is of course the dragon guarding the treasure at the center of our quest.

To slay the dragon, we need to produce a cohesive story that at least matches LegacyMode's features, while remaining flexible enough to allow us to expand it to additional features later.

On the surface, this looks like adding an equivalent for sync/async hasMany/belongsTo with/without inverses (six total permutations).

However - in practice we've come to discover that the way apps use relationships today includes two categories of relationships we never intended to support but for which we must plan a future. We'll call these pointers and references.

Pointers and references are both forms relationships where the related resources are presumed to have been loaded previously via a separate request. Pointers presume that the related record MUST have been loaded, while references allow for the related resource to be missing.

Any of the 6 permutations of relationship in LegacyMode could have been functioning as either a pointer or reference in disguise which would indicate 18 total relationship permutations (eeek). However, for our purposes we are going to enforce that pointers and references MUST be synchronous and MUST utilize inverse: null semantics. In other words a sync hasMany/belongsTo reference/pointer without an inverse => 4 combos for a total of 10 relationship permutations that we need to support in PolarisMode (not counting paginated hasMany).

I believe the minimum requirement is to ship the base 6, followed by adding in these 4 newly discovered permutations post-declaration of PolarisMode being ready - and with paginated hasMany to follow at a later unspecified time.

runspired avatar Oct 01 '25 07:10 runspired

Pointers and references are both forms relationships where the related resources are presumed to have been loaded previously via a separate request. Pointers presume that the related record MUST have been loaded, while references allow for the related resource to be missing.

What would be an example of this in legacy mode?

// user.js

export default class UserModel extends BaseModel {
  @hasMany('group', { async: false, inverse: 'members' }) groups;
}

// some other file

// is this an example of code assuming groups are already loaded?
user.groups.forEach(…)

// or this?
await user.groups.forEach(…)

sandstrom avatar Oct 02 '25 07:10 sandstrom

@sandstrom

In the LegacyMode world, what we are now calling pointers and references were always "sync" relationships, not async. Because async: true could be relied on to autofetch - though you would see "tearing" - even computeds deriving off of them would eventually always get to a generally correct state (some caveats about distributed systems yada yada apply).

Both pointers and references typically occurred for either (or several!) of four reasons:

  • systemic knowledge allowing the developer to know that a previous or concurrent request would deliver the related records
  • use of the storefront pattern which encouraged having relationships marked synchronous that were actually asynchronous.
  • use of the References API to avoid loading more than just the ID unless explicitly desired
  • bad API design and no good way to make it work

In PolarisMode, we want to provide stricter checks and stronger guarantees around the data you are working with. So if a relationship is marked synchronous and you have the type/id of the related thing - the related thing should be included in the same payload. And if a relationship is marked asynchronous, it should either have a link to fetch or a link + the data (with the same rule on being included as for sync).

In order to enforce the above - we need a way for developers to explicitly tell us when they are expecting a relationship to behave differently and not meet those expectations. This could be an option on the relationship - but by making them distinct fields its much more clear that they behave differently. And also - by making it explicit, we can similarly add clearer checks and validation to help developers spot issues like "you'd promised this thing was already present but it isn't".

I personally think pointers and references are especially important in the age of AI driven recommendations and features. Often suggestions are streaming in from a wholly distinct API that knows something to reference - like just an ID - but isn't able to give you the full representation or even a link, being a separate non-integrated system.

runspired avatar Oct 02 '25 08:10 runspired