-
Notifications
You must be signed in to change notification settings - Fork 12
Pin the suite's clock so a rendered elapsed time stops drifting #6043
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
Open
backspace
wants to merge
7
commits into
cs-12770-guard-ambient-clock
Choose a base branch
from
cs-12770-pin-test-clock
base: cs-12770-guard-ambient-clock
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
9fe736f
Pin the suite's clock so a rendered elapsed time stops drifting
backspace 040e8dc
Advance fixture mtimes, and measure the calendar test from the pinned…
backspace 8f2f649
Share one clock across the card-loader boundary
backspace c92ad48
Give the clock re-export the file extension this package requires
backspace bb4300d
Keep seeded fixture mtimes inside the just-now window, per realm
backspace 7663940
Group the test-clock import with the other sibling imports
backspace 32bdae0
Merge remote-tracking branch 'origin/cs-12770-guard-ambient-clock' in…
backspace File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,48 +1,7 @@ | ||
| // The instant card code measures elapsed time from. | ||
| // | ||
| // Left alone this is the real clock. Setting a number on `globalThis.__boxelNow` | ||
| // pins it, so what a card renders becomes a function of its own data rather | ||
| // than of when it happened to be rendered. | ||
| // | ||
| // The number is epoch **milliseconds**, the same units `Date.now()` returns. | ||
| // Worth stating because this package stamps a file's `lastModified` and | ||
| // `resourceCreatedAt` in epoch *seconds*, and `toDate` in | ||
| // `file-formats/file-presentation` exists to promote those — so seconds are | ||
| // the value nearest to hand for someone reaching for a pin. A seconds-valued | ||
| // pin lands in 1970, puts every real timestamp in its future, and turns every | ||
| // age into an absolute date through the `days < 0` branch. It throws nothing | ||
| // and produces no NaN; a snapshot diff reads it as a formatting change. | ||
| // | ||
| // `now()` deliberately does not promote seconds the way `toDate` does. A file | ||
| // mtime below the year-2001 millisecond floor is unambiguous, but a pin is | ||
| // chosen by its caller, and guessing would hide the mistake rather than | ||
| // surface it. | ||
| // | ||
| // That distinction matters for anything whose output is an age — `3d ago`, a | ||
| // countdown, an "expires soon" warning, a `Today` / `Yesterday` day header. | ||
| // Each changes on a schedule nobody chose, so a visual comparison of one | ||
| // differs between two runs over identical data, and the usual way to quiet | ||
| // that is to stop comparing the element at all. Pinning the clock instead | ||
| // keeps the value visible, so a change in how it is formatted still registers. | ||
| // | ||
| // Reading through here is what makes a renderer pinnable; a renderer that | ||
| // calls the clock directly is not, whatever this module says. The rule that | ||
| // keeps them converted lives with the lint config rather than in this comment, | ||
| // because a list of callers here would be wrong the moment one is added. | ||
| // | ||
| // Read off `globalThis` rather than taken as an argument because it has to | ||
| // reach card code through whichever loader instance rendered it, which no call | ||
| // site knows about. `__boxelRenderContext` and its neighbours already work | ||
| // this way. | ||
| export function now(): number { | ||
| let pinned = (globalThis as { __boxelNow?: unknown }).__boxelNow; | ||
| return typeof pinned === 'number' && Number.isFinite(pinned) | ||
| ? pinned | ||
| : Date.now(); | ||
| } | ||
|
|
||
| // The same instant as a `Date`, for callers that would otherwise write | ||
| // `new Date()`. | ||
| export function nowDate(): Date { | ||
| return new Date(now()); | ||
| } | ||
| // The implementation is in `@cardstack/runtime-common`, because the host app | ||
| // measures the same timestamps and needs the same instant; this is the path | ||
| // card code imports it by. See that module for what the pin is, what units it | ||
| // takes, and why it is read off a global. | ||
| export { now, nowDate } from '@cardstack/runtime-common'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| // The instant the suite reports as "now". | ||
| // | ||
| // Card code reads the clock through `@cardstack/base/helpers/clock`, which | ||
| // falls through to the real one unless something pins it. Pinning it here | ||
| // makes what a card renders a function of its own data: "3d ago", a countdown, | ||
| // an age and an "expires soon" warning all stop depending on when the suite | ||
| // happened to run, so a visual comparison of them is a comparison of the code | ||
| // rather than a race against whichever threshold each is nearest. | ||
| // | ||
| // The value is arbitrary — determinism is the point, not realism — but it is | ||
| // not free of consequences, so it is chosen rather than picked. Two things | ||
| // follow from it: | ||
| // | ||
| // Realms the tests build in the browser stamp their files with this same | ||
| // instant (see `adapter.ts`), so those files read as `today`, which is what | ||
| // they read as when both sides were the real clock. Nothing about them moves. | ||
| // | ||
| // Realms served from the index cache carry mtimes that | ||
| // `scripts/normalize-realm-mtimes.mjs` derives from file content, so they are | ||
| // fixed dates scattered across decades. Their rendered ages are measured from | ||
| // this instant instead of from today, which shifts some of them by a unit the | ||
| // first time this lands. That is a one-time baseline change, and it buys the | ||
| // property that they never shift again on their own. | ||
| // Mid-month and mid-day on purpose. A calendar test asserts that days earlier | ||
| // in the current month are disabled by a `today` sentinel, and guards itself | ||
| // with `getDate() > 1` — pinned to the first, that assertion would skip rather | ||
| // than fail, which is a quieter way to lose it than leaving it broken. | ||
| export const TEST_CLOCK_INSTANT = Date.UTC(2026, 8, 15, 12, 0, 0); | ||
|
|
||
| export function pinTestClock() { | ||
| (globalThis as { __boxelNow?: number }).__boxelNow = TEST_CLOCK_INSTANT; | ||
| } | ||
|
|
||
| // A realm's fixture mtimes, as a fresh sequence per adapter. | ||
| // | ||
| // Two constraints pull against each other here. | ||
| // | ||
| // They must advance: the indexer decides what a from-scratch pass has to | ||
| // revisit by comparing a file's mtime against the one on its index row and | ||
| // skipping where they match, so a write that leaves the mtime alone is an edit | ||
| // it cannot see. | ||
| // | ||
| // They must also stay within a minute of the pinned instant, because | ||
| // `formatLastSavedText` calls anything closer than that "just now" and the | ||
| // inspector asserts a seeded fixture reads that way — which it did when both | ||
| // the clock and the stamp were the real one, the files having been written | ||
| // moments earlier. | ||
| // | ||
| // A minute of one-second steps is only sixty values, which a whole shard would | ||
| // exhaust immediately. Per adapter it is ample: a realm seeds once and a test | ||
| // writes a handful of times. Sequences in different realms overlap, which | ||
| // costs nothing — the comparison that matters is between a file and its own | ||
| // index row. | ||
| const FIXTURE_MTIME_SPAN_S = 59; | ||
|
|
||
| export function createFixtureMtimeSequence(): () => number { | ||
| let pinnedSeconds = Math.floor(TEST_CLOCK_INSTANT / 1000); | ||
| let step = 0; | ||
| return () => | ||
| Math.min(pinnedSeconds - FIXTURE_MTIME_SPAN_S + step++, pinnedSeconds - 1); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| // The instant anything measuring elapsed time should measure from. | ||
| // | ||
| // Unset, this is the real clock. Setting a number on `globalThis.__boxelNow` | ||
| // pins it, so what gets rendered becomes a function of the data rather than of | ||
| // when it was rendered. | ||
| // | ||
| // The number is epoch **milliseconds**, the same units `Date.now()` returns. | ||
| // Worth stating because a file's `lastModified` and `resourceCreatedAt` are | ||
| // stamped in epoch *seconds*, and `toDate` in | ||
| // `base/file-formats/file-presentation` exists to promote those — so seconds | ||
| // are the value nearest to hand for someone reaching for a pin. A | ||
| // seconds-valued pin lands in 1970, puts every real timestamp in its future, | ||
| // and turns every age into an absolute date. It throws nothing and produces no | ||
| // NaN; a snapshot diff reads it as a formatting change. | ||
| // | ||
| // `now()` deliberately does not promote seconds the way `toDate` does. A file | ||
| // mtime below the year-2001 millisecond floor is unambiguous, but a pin is | ||
| // chosen by its caller, and guessing would hide the mistake rather than | ||
| // surface it. | ||
| // | ||
| // That matters wherever output is an age — "3d ago", a countdown, "Last saved | ||
| // 6 days ago", a `Today` / `Yesterday` day header. Each changes on a schedule | ||
| // nobody chose, so a visual comparison of one differs between two runs over | ||
| // identical data, and the usual way to quiet that is to stop comparing the | ||
| // element, which trades the coverage away. | ||
| // | ||
| // Reading through here is what makes a renderer pinnable; one that calls the | ||
| // clock directly is not, whatever this comment says. The rule that keeps them | ||
| // converted lives with the lint config, because a list of callers here would | ||
| // be wrong the moment one is added. | ||
| // | ||
| // It lives in runtime-common rather than in either consumer because both sides | ||
| // of the card loader need it: card code reaches it through | ||
| // `base/helpers/clock`, and the host app imports it directly. A global is what | ||
| // crosses that boundary — the loader gives card code its own module instances, | ||
| // so a shared module-level variable would not be shared at all. | ||
| // `__boxelRenderContext` and its neighbours already work this way. | ||
| export function now(): number { | ||
| let pinned = (globalThis as { __boxelNow?: unknown }).__boxelNow; | ||
| return typeof pinned === 'number' && Number.isFinite(pinned) | ||
| ? pinned | ||
| : Date.now(); | ||
| } | ||
|
|
||
| // The same instant as a `Date`, for callers that would otherwise write | ||
| // `new Date()`. | ||
| export function nowDate(): Date { | ||
| return new Date(now()); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.