-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
fix: improved fine-grainability of ReactiveDate #12110
Conversation
🦋 Changeset detectedLatest commit: ac907f0 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
This is a pretty significant increase in complexity that adds performance and memory overhead. I'm not personally convinced it's worth it — fine-grained-ness is an optimisation (i.e. it's not a correctness issue), and as such if it makes common cases less optimal then it's probably something that should be avoided. If it's important to avoid invalidating something, it's easy enough to do it like this: let year = $derived(date.getFullYear()); |
@Rich-Harris made it a tad more performant, but if it still doesn't worth feel free to close it. Also just wanted to mention that it will only create signals when the method is actually called so we don't create signals on initialization. Edit: I feel like if we think this way, lots of things aren't fine-grained in reactivity package, so can we just remove it and let the user create one? cuz these kinds of behaviors breaks my expectations of what reactive date or set means; or add these behaviors to docs so people are aware? Edit2: |
I don't really think it's required. I think of Date as a timestamp wrapper with a bunch of methods to format it, while Set, Map, and URLSearchParams are collections. Also, you or somebody else can always create a package with super fine-grained native types. |
What if we instead just keep mostly what we have today already, but instead of reading only the time value, we read the time value as part of a derived that also returns the full value. Then wouldn't each API be fine-grain whilst not needing much in the way of anything? Date is kind of unique in that you normally only listen to a handful of things, so creating a fresh derived each time is likely the least expensive and least complicated thing to do. |
If I undertood you correctly, then I don't think that would be the case. If you do |
I mean it would create a separate derived signal and return that from doing i.e. for (const method of read) {
// @ts-ignore
proto[method] = function (...args) {
const derived_value = derived(() => {
get(this.#raw_time);
// @ts-ignore
return date_proto[method].apply(this, args);
});
return get(derived_value);
};
} |
38a33da
to
0703693
Compare
Ow now I get it :D It would greatly simplify the logic. I stole your idea (bowahaha) and merged it with mine which uses less signals
I guess its ok now? >_< |
You definitely don't want to store them in a map, otherwise you'll just retain too much. Just create a new one each time the method is called. |
@trueadm out of curiosity, doesn't that lead to creating more signals? If I call |
@FoHoOV It would but deriveds clean themselves up. However, I think you're also right in that we probably want to reduce the amount of deriveds being created. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good now but 5 tests fail locally for me — lots of off-by-one errors owing to the timezone I'm in. Pretty sure we just need to tweak the tests, but it's eluding me
@Rich-Harris @trueadm Sorry I was away for a couple of days. The issue is funny actually xD I hate const initial_date = new Date('2023-01-01T00:00:00.000+0000'); calling the test('date.setDate and date.setUTCDate', () => {
const date = new SvelteDate(initial_date);
const log: any = [];
const cleanup = effect_root(() => {
render_effect(() => {
log.push(date.getMonth());
});
render_effect(() => {
log.push(date.getUTCMonth());
});
});
flushSync(() => {
date.setMonth(initial_date.getMonth() + 5);
});
flushSync(() => {
date.setMonth(initial_date.getMonth() + 5); // no change expected
});
flushSync(() => {
date.setUTCMonth(initial_date.getUTCMonth() + 10);
});
assert.deepEqual(log, [
// something
]);
cleanup();
}); if you timezone is lets say -08:00 (US & Canada) then initial date will be in There are multiple ways to overcome this issue, but I've come into two solutions which don't hack around how
const initial_date = new Date('2023-01-01T00:00:00.000+0000');
function apply_change_to_date(time: number, set_method_name: DateMethods<'set'>, value: number) {
const date = new Date(time);
date[set_method_name](value);
return date;
}
test('date.getMonth and date.setUTCMonth', () => {
const date = new SvelteDate(initial_date);
let expected: { utc: number | null; local: number | null } = {
local: initial_date.getMonth(),
utc: initial_date.getUTCMonth()
};
const cleanup = effect_root(() => {
render_effect(() => {
assert.equal(expected.local, date.getMonth());
expected.local = null;
});
render_effect(() => {
assert.equal(expected.utc, date.getUTCMonth());
expected.utc = null;
});
render_effect(() => {
assert.isNull(expected.local);
assert.isNull(expected.utc);
});
});
flushSync(() => {
const new_value = initial_date.getMonth() + 5;
const new_date = apply_change_to_date(initial_date.getTime(), 'setMonth', new_value);
expected = { local: new_date.getMonth(), utc: new_date.getUTCMonth() };
date.setMonth(new_value);
});
flushSync(() => {
const new_value = initial_date.getMonth() + 5;
const new_date = apply_change_to_date(initial_date.getTime(), 'setMonth', new_value);
expected = { local: new_date.getMonth(), utc: new_date.getUTCMonth() };
date.setMonth(new_value);
});
flushSync(() => {
const new_value = initial_date.getUTCMonth() + 10;
const new_date = apply_change_to_date(date.getTime(), 'setUTCMonth', new_value);
expected = { local: new_date.getMonth(), utc: new_date.getUTCMonth() };
date.setUTCMonth(new_value);
});
cleanup();
});
test('date.setX and date.setUTCX', () => {
const date_prop_names = Object.getOwnPropertyNames(Date.prototype);
const utc_set_methods = date_prop_names.filter((name) =>
name.startsWith('setUTC')
) as unknown as Array<DateMethods<'setUTC'>>;
utc_set_methods.forEach((utc_set_method) => {
const svelte_date = new SvelteDate(initial_date);
const utc_get_method = utc_set_method.replace('set', 'get') as unknown as DateMethods<'getUTC'>;
const local_set_method = utc_set_method.replace('UTC', '') as unknown as DateMethods<
'set',
'UTC'
>;
const local_get_method = utc_get_method.replace('UTC', '') as unknown as DateMethods<
'get',
'UTC'
>;
let expected: { utc: number | null; local: number | null } = {
utc: initial_date[utc_get_method](),
local: initial_date[local_get_method]()
};
const cleanup = effect_root(() => {
render_effect(() => {
const value = svelte_date[utc_get_method]();
assert.equal(value, expected.utc, `in effect for ${utc_get_method}`);
expected.utc = null;
});
render_effect(() => {
const value = svelte_date[local_get_method]();
assert.equal(value, expected.local, `in effect for ${local_get_method}`);
expected.local = null;
});
});
render_effect(() => {
// expect both local and utc to be null at point, because
// if they they have a value at this point, then it means the effect,
// didn't run for them
assert.isNull(expected.local);
assert.isNull(expected.utc);
});
flushSync(() => {
const new_value = initial_date[local_get_method]() + 5;
const new_date = apply_change_to_date(initial_date.getTime(), local_set_method, new_value);
expected = { local: new_date[local_get_method](), utc: new_date[utc_get_method]() };
svelte_date[local_set_method](new_value);
});
flushSync(() => {
const new_value = initial_date[local_get_method]() + 5;
svelte_date[local_set_method](new_value);
// no rerun expected here
});
flushSync(() => {
const new_value = initial_date[utc_get_method]() + 10;
const new_date = apply_change_to_date(svelte_date.getTime(), utc_set_method, new_value);
expected = { local: new_date[local_get_method](), utc: new_date[utc_get_method]() };
svelte_date[utc_set_method](new_value);
});
cleanup();
});
}); I tested both of them on many different time-zones. If you liked the solution I can apply the changes >_< - You guys always come up with something 1000x simpler 🗡️ |
I just took another run at this — I adjusted the last couple of tests that weren't passing (earlier I had changed the sample dates from |
@Rich-Harris creating dates like that is actually based on local timezone, here is the MDN reference (see "Individual date and time component values" part.
Why it works is because of the initial date: new Date(2023, 0, 2, 0, 0, 0, 0); Since it starts at day 2 of year 2023, then for instance "-12:00" will not change the month or year, so it will work. I thought changing the intial value feels like a hack (as mentioned in the comment above). It seems there are no edge cases that will fail because of this :D Thanks for your "date" and time >_< |
Currently any changes to
ReactiveDate
class causes every read method to be notified, this should be making them more fine-grained. This PR also adds tests for Date class (none UI).fixed Date part of #11727
Svelte 5 rewrite
Please note that the Svelte codebase is currently being rewritten for Svelte 5. Changes should target Svelte 5, which lives on the default branch (
main
).If your PR concerns Svelte 4 (including updates to svelte.dev.docs), please ensure the base branch is
svelte-4
and notmain
.Before submitting the PR, please make sure you do the following
feat:
,fix:
,chore:
, ordocs:
.Tests and linting
pnpm test
and lint the project withpnpm lint