IntelliTect / IntelliTect/Coalesce

Use Temporal instead of Date

Open
#694 0 comments 0 reactions 0 assignees View on GitHub
breaking enhancement
Dominant language
C#
Stars
82
Forks
24
Avg merge
2d 19h
Merged PRs (30d)
5

Description

AI generated summary of migration path. Some notes/thoughts:
- Would like to use new metadata type specifiers instead of "date" + "dateKind"/"noOffset" flags
- Does DateTimeOffset actually map to Instant rather than ZonedDateTime? Probably not, the ergonomics of Instant are more annoying, it isn't as "ready" of a type to work with as ZDT.

-----

## Temporal Migration: Scope & Strategy

### Type Mapping

The natural mapping from C# types → Temporal types would be:

| C# Type | Current JS Type | Temporal Type | `dateKind` | `noOffset` |
|---|---|---|---|---|
| `DateTimeOffset` | `Date` | `Temporal.ZonedDateTime` | `"datetime"` | `false` |
| `DateTime` | `Date` | `Temporal.PlainDateTime` | `"datetime"` | `true` |
| `DateOnly` | `Date` | `Temporal.PlainDate` | `"date"` | `true` |
| `TimeOnly` | `Date` (Jan 1 hack) | `Temporal.PlainTime` | `"time"` | `true` |

This is the single biggest win of Temporal: the _type itself_ encodes what the current code uses `dateKind`/`noOffset` metadata to disambiguate at runtime. The Jan 1 hack in `parseJSONDate` for `TimeOnly` goes away entirely.

### Architecture Assessment

The codebase is **well-positioned** for this change because date handling is centralized through a visitor pattern and a small number of core functions:

**Core bottleneck functions (coalesce-vue):**
1. **`parseJSONDate()`** in util.ts — would return different Temporal types based on `dateKind`
2. **`parseDateUserInput()`** in util.ts — same
3. **`parseValue()`** in model.ts — return type changes from `Date` to union of Temporal types
4. **`MapToDtoVisitor.visitDateValue()`** in model.ts — serialization, replaces `lightFormat`/`format`/`formatInTimeZone`
5. **`DisplayVisitor.visitDateValue()`** in model.ts — display formatting

### Changes Required

#### 1. **Type System** (metadata.ts)
- `TypeDiscriminatorToType<"date">` currently maps to `Date` — would become `Temporal.PlainDate | Temporal.PlainTime | Temporal.PlainDateTime | Temporal.ZonedDateTime`
- Could alternatively split `"date"` into separate type discriminators (`"plainDate"`, `"plainTime"`, etc.), but that's a much larger metadata/codegen change
- **Decision needed**: keep single `"date"` discriminator or split?

#### 2. **Parsing** (util.ts)
- `parseJSONDate()` would return different Temporal types instead of always `Date`
- The Safari `new Date()` workaround becomes irrelevant — Temporal has its own `from()` methods
- `parseDateUserInput()` currently returns `Date` — would return corresponding Temporal type
- **Eliminates** the Jan 1 hack for TimeOnly and the manual regex parsing

#### 3. **Serialization** (model.ts)
- `MapToDtoVisitor.visitDateValue()` — `lightFormat()` / `format()` / `formatInTimeZone()` replaced by Temporal's `.toString()` methods which produce ISO 8601 natively
- **Eliminates** the need for date-fns for serialization

#### 4. **Display** (model.ts)
- `DisplayVisitor.visitDateValue()` — `toLocaleString()` works on Temporal types with `Intl.DateTimeFormat`
- `formatDistanceToNow()` from date-fns would need a replacement (Temporal doesn't have this natively)
- Custom format strings currently using date-fns patterns — would need `Intl.DateTimeFormat` or a Temporal-aware formatting library

#### 5. **ViewModel equality checks** (viewmodel.ts)
- `.valueOf()` comparisons — would use `Temporal.*.compare()` instead

#### 6. **UI Components** (coalesce-vue-vuetify3)
- **c-datetime-picker.vue** — Heaviest changes. Uses `date-fns` extensively (`setYear`, `setMonth`, `setDate`, `setHours`, `startOfDay`, `endOfDay`, `addDays`, `parse`, `format`). Temporal has `.with()` and `.add()` that replace most of these.
- **c-time-picker.vue** — `.getHours()`, `.getMinutes()` → `PlainTime.hour`, `.minute`
- **Vuetify `v-date-picker`** — **This is a blocker.** Vuetify's `VDatePicker` works with `Date` objects. Would need to convert at the boundary (Temporal ↔ Date) or wait for Vuetify Temporal support.
- **c-select.vue** — Uses `Date` for animation timing (`new Date().valueOf()`), not date data. These should stay as `Date` or use `performance.now()`.

#### 7. **Code Generation** (TsMetadata.cs)
- The generated metadata shape may not need to change much if keeping the `"date"` discriminator
- Generated model types would need to reference Temporal types instead of `Date`

#### 8. **date-fns Dependency**
- `date-fns` and `date-fns-tz` are currently used for: formatting, parsing, timezone conversion, relative time, date arithmetic
- Temporal replaces most of this natively **except** locale-aware formatting with custom patterns and relative time (`formatDistanceToNow`)
- Could potentially drop `date-fns-tz` entirely. `date-fns` might be reduced to just `formatDistanceToNow` or dropped if using `Intl.RelativeTimeFormat`

### Key Risks / Considerations

1. **Browser support**: Temporal is Stage 3 and shipping in browsers as of 2025 (Chrome 144+, Firefox 139+, ~~Safari 18.4+~~). Older browsers would need the `@js-temporal/polyfill` (~40KB gzipped). Polyfill performance for the hot path (`parseJSONDate` called on every API response) needs benchmarking.

2. **Vuetify compatibility**: `VDatePicker` expects `Date`. Need a conversion layer at the component boundary, or Vuetify needs to add Temporal support.

3. **Breaking change surface**: Every consumer that reads a date property from a Coalesce model gets a Temporal type instead of `Date`. This affects:
- Custom display components
- Business logic in app code
- Any direct `.getFullYear()`, `.getMonth()`, etc. calls
- Any code passing Coalesce dates to APIs expecting `Date`

4. **`parseValue()` return type**: Currently `parseValue(value, DateValue): Date | null`. Would become a union type, which is harder to consume unless the discriminator is split.

5. **Incremental migration path**: Could introduce a `useTemporal` flag on metadata or a global config, but dual-mode support adds significant complexity.

### Recommendation

The migration is **feasible and well-contained** thanks to the visitor pattern architecture. The main work centers on ~5 core functions in coalesce-vue and ~2 UI components in coalesce-vue-vuetify3. The biggest external dependency risk is Vuetify's `v-date-picker`.

**Estimated scope**: ~15-20 files changed across both packages, plus code generation updates, plus test updates. The hardest part is not the Coalesce code itself — it's the breaking change impact on downstream consumers.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.