Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recommended patterns for detecting and cloning any Temporal object #1841

Open
justingrant opened this issue Sep 21, 2021 · 12 comments
Open

Recommended patterns for detecting and cloning any Temporal object #1841

justingrant opened this issue Sep 21, 2021 · 12 comments
Labels
documentation Additions to documentation
Milestone

Comments

@justingrant
Copy link
Collaborator

justingrant commented Sep 21, 2021

While testing Temporal in one of my apps, I ran into a failure that turned out to be caused by the clone library being unable to deep-clone objects that included Temporal instances as properties. Looking at the source of clone, it's clear why cloning won't work for Temporal instances. (Nor, I assume, with many other types like those that use private fields, WeakMaps, etc.)

There are a lot of similar libraries that do deep cloning. I assume that all of these libraries will need to be updated to know about Temporal, just like they needed to be updated to recognize other ES built-in types like Map and Set.

I assume it would be helpful for us to provide a recommended implementation of "Is this a Temporal instance?" and "clone this Temporal instance" code. I built a simplistic draft of these operations below. What am I missing?

In particular, is it better to identify "is this a Temporal instance" by comparing the constructor property to Temporal constructors? Or is it better to be more loose, like this?

o?.[Symbol.toStringTag] === 'Temporal.Instant'

Here's a starting point:

const temporalTypes = [
  Temporal.Instant,
  Temporal.ZonedDateTime,
  Temporal.PlainDate,
  Temporal.PlainTime,
  Temporal.PlainDateTime,
  Temporal.PlainYearMonth,
  Temporal.PlainMonthDay,
  Temporal.Duration,
  Temporal.TimeZone,
  Temporal.Calendar
];
function isTemporalInstance(o) {
  if (!o || !o.constructor) return false;
  return temporalTypes.includes(o.constructor);
}
function cloneTemporalInstance(o) {
  if (!o || !o.constructor) return undefined;
  const constructor = temporalTypes.find(t => t === o.constructor);
  return constructor ? constructor.from(o) : undefined;
}
@ljharb
Copy link
Member

ljharb commented Sep 21, 2021

I would definitely hope there's something more reliable than Symbol.toStringTag to determine "is X a temporal instance". Aren't there brand-checking getters that check for a common internal slot, that can be try/catched?

@justingrant
Copy link
Collaborator Author

I don't think there's a single slot that's present in all Temporal types, but other champions can correct me if I'm wrong.

As an aside, using try/catch for this purpose can be frustrating for developers using browser or IDE debuggers because it complicates "break on caught exceptions" workflows for frameworks like React where exceptions are swallowed by the framework, I assume to make non-debugger error handling have better DX? Anyway, debuggers have different ways to defeat this (e.g. by letting users label some code that should be ignored when exceptions are thrown there) but AFAIK there's no automated way that debuggers can hide these "expected exceptions" from users without manual intervention. It'd be great to not make this problem worse if possible.

@ljharb
Copy link
Member

ljharb commented Sep 21, 2021

Failing that, I'd expect that such a thing exists for each Temporal type (more to the point, this part is an axiom of the language that this is true, so it's a serious normative bug if it's not).

"break on caught exceptions" is already unrealistic, and the Way that brand checks are done in JS is, sadly, try/catch - see https://npmjs.com/~inspect-js.

@ptomato
Copy link
Collaborator

ptomato commented Sep 23, 2021

@ljharb Which part are you saying is an axiom of the language? That a dedicated method exists for brand-checking, or just that it is possible?

Currently it is possible, every method on a Temporal object does a brand-check, so you can do something like this

const ReflectApply = Reflect.apply;
const TemporalInstantPrototypeToString = Temporal.Instant.prototype.toString;
// ...etc.

function isTemporalInstant(value) {
  try {
    ReflectApply(TemporalInstantPrototypeToString, value, []);
  } catch (e) {
    return false;
  }
  return true;
}

@justingrant I think what you are asking for is something different? That is, a Temporal "brand-check family" and a way to check if an object's brand is part of that family?

@ljharb
Copy link
Member

ljharb commented Sep 23, 2021

@ptomato i'm told today that "invariant" is a better term to use than "axiom" :-) I'm saying it's an invariant that brand checking be possible. The only exception - due to oversight - is Error, and the stack traces proposal would fill that gap.

I agree that it seems like @justingrant wants something that can check the bag of Temporal types all at once instead of individually hardcoding each brand check (which is what i'd otherwise do in the various https://npmjs.com/~inspect-js packages i will be making)

@ptomato
Copy link
Collaborator

ptomato commented Oct 1, 2021

I have been doing some more reading on invariants and I think it'd be useful for future discussions to update https://github.com/codehag/documenting-invariants/blob/master/abandoned_invariants.md#objectprototypetostringcall-as-a-brand to reflect this. It doesn't seem correct that that's listed under "abandoned invariants", if it was accidentally broken and it should still hold for any future types?

@ljharb
Copy link
Member

ljharb commented Oct 1, 2021

@ptomato Object.prototype.toString isn't the existing invariant, "brand checking" is - pre-ES6, Object.prototype.toString was just the most common/simple way to do that brand check.

It was intentionally broken, and the committee decided not to withdraw Symbol.toStringTag from ES6, and instead agreed to always provide a brand-checking mechanism on every built-in type. The only accident was the omission of Error.

@ptomato
Copy link
Collaborator

ptomato commented Oct 1, 2021

I think we are in agreement! I'm trying to say, the following text from that link:

a new invariant was added at that time, that every builtin type must have a means by which it can be brand-checked, even if indirectly

which sounds like the same thing you are saying, should be documented clearly in that repo, and not listed as part of "abandoned invariants" as it is now. (Even if the rest of the text about Object.prototype.toString no longer holds)

@ljharb
Copy link
Member

ljharb commented Oct 2, 2021

Ah, totally agree then :-)

@ptomato
Copy link
Collaborator

ptomato commented Oct 4, 2021

@ljharb Would you like to take a crack at it? It sounds like you know all the context for this invariant.

@justingrant What do you think would be the best resolution for this issue? A cookbook recipe and/or a discussion topic for temporal-v2?

@justingrant
Copy link
Collaborator Author

Cookbook recipe sounds good.

@ljharb
Copy link
Member

ljharb commented Oct 4, 2021

@ptomato codehag/documenting-invariants#13

@ptomato ptomato added the documentation Additions to documentation label May 10, 2022
@ptomato ptomato added this to the Post Stage 4 milestone Dec 8, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Additions to documentation
Projects
None yet
Development

No branches or pull requests

3 participants