@buenos-nachos/time-sync 0.5.4 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,171 +0,0 @@
1
- import { afterEach, beforeEach, describe, it, vi } from "vitest";
2
- import { ReadonlyDate } from "./ReadonlyDate";
3
-
4
- beforeEach(() => {
5
- vi.useFakeTimers();
6
- });
7
-
8
- afterEach(() => {
9
- vi.restoreAllMocks();
10
- });
11
-
12
- const defaultDateString = "October 27, 2025";
13
-
14
- // new ReadonlyDate is mostly being treated as an internal implementation
15
- // detail for the moment, but because we still export it for convenience,
16
- // we need to make sure that it's 100% interchangeable with native Date
17
- // objects for all purposes aside from mutations
18
- describe(ReadonlyDate, () => {
19
- it("Appears as native Date type for external consumers", ({ expect }) => {
20
- const d = new ReadonlyDate();
21
- expect(d).toBeInstanceOf(Date);
22
- });
23
-
24
- // Asserting this first because we rely on this behavior for the other tests
25
- it("Supports .toEqual checks against native Dates in test runners", ({
26
- expect,
27
- }) => {
28
- const controlDate = new Date(defaultDateString);
29
- const readonly = new ReadonlyDate(defaultDateString);
30
- expect(controlDate).toEqual(readonly);
31
- });
32
-
33
- it("Mirrors type signature of native Dates", ({ expect }) => {
34
- // Have to save the version without arguments for last, because it
35
- // requires the most mocking, and has a risk of breaking the other cases
36
- type TestCase = Readonly<{
37
- input: readonly (number | string | Date)[];
38
- expected: Date;
39
- }>;
40
- const cases = [
41
- {
42
- input: [752_475_600_000],
43
- expected: new Date("November 5, 1993"),
44
- },
45
- {
46
- input: ["September 4, 2000"],
47
- expected: new Date("September 4, 2000"),
48
- },
49
- {
50
- input: [new Date("January 8, 1940")],
51
- expected: new Date("January 8, 1940"),
52
- },
53
- {
54
- input: ["2006-11-01T05:00:00.000Z"],
55
- expected: new Date("2006-11-01T05:00:00.000Z"),
56
- },
57
- {
58
- input: [2009, 10],
59
- expected: new Date("November 1, 2009"),
60
- },
61
- {
62
- input: [2008, 2, 4],
63
- expected: new Date("March 4, 2008"),
64
- },
65
- {
66
- input: [2000, 1, 1, 5],
67
- expected: new Date("2000-02-01T10:00:00.000Z"),
68
- },
69
- {
70
- input: [1990, 0, 5, 20, 6],
71
- expected: new Date("1990-01-06T01:06:00.000Z"),
72
- },
73
- {
74
- input: [2000, 10, 8, 5, 17, 20],
75
- expected: new Date("2000-11-08T10:17:20.000Z"),
76
- },
77
- {
78
- input: [2005, 7, 4, 20, 37, 57, 3],
79
- expected: new Date("2005-08-05T00:37:57.003Z"),
80
- },
81
- ] satisfies readonly TestCase[];
82
-
83
- for (const { input, expected } of cases) {
84
- // @ts-expect-error -- This should always work at runtime, but the
85
- // TypeScript compiler isn't smart enough to figure that out
86
- const readonly = new ReadonlyDate(...input);
87
- expect(readonly).toEqual(expected);
88
- }
89
-
90
- const control = new Date(defaultDateString);
91
- vi.setSystemTime(control);
92
- const withoutArgs = new ReadonlyDate();
93
- expect(withoutArgs).toEqual(control);
94
- });
95
-
96
- it("Turns all mutation methods into no-ops", ({ expect }) => {
97
- const source = new ReadonlyDate(defaultDateString);
98
- const copyBeforeMutations = new ReadonlyDate(source);
99
-
100
- const setTests: readonly (() => void)[] = [
101
- () => source.setDate(4_932_049_023),
102
- () => source.setFullYear(2000),
103
- () => source.setHours(50),
104
- () => source.setMilliseconds(499),
105
- () => source.setMinutes(45),
106
- () => source.setMonth(3),
107
- () => source.setSeconds(40),
108
- () => source.setTime(0),
109
- () => source.setUTCDate(3),
110
- () => source.setUTCFullYear(1994),
111
- () => source.setUTCHours(7),
112
- () => source.setUTCMilliseconds(45),
113
- () => source.setUTCMinutes(57),
114
- () => source.setUTCMonth(3),
115
- () => source.setUTCSeconds(20),
116
- ];
117
- for (const test of setTests) {
118
- test();
119
- }
120
-
121
- expect(source).toEqual(copyBeforeMutations);
122
- });
123
-
124
- it("Can be instantiated via other ReadonlyDates", ({ expect }) => {
125
- const first = new ReadonlyDate(defaultDateString);
126
- const derived = new ReadonlyDate(first);
127
- expect(first).toEqual(derived);
128
- });
129
-
130
- it("Can be converted to a native date", ({ expect }) => {
131
- const d = new ReadonlyDate("February 5, 1770");
132
- const converted = d.toNativeDate();
133
-
134
- expect(d).toEqual(converted);
135
- expect(converted).toBeInstanceOf(Date);
136
- expect(converted).not.toBeInstanceOf(ReadonlyDate);
137
- });
138
-
139
- it("Throws when provided invalid input (instead of failing silently like with native dates)", ({
140
- expect,
141
- }) => {
142
- const invalidDate = new Date(Number.NaN);
143
- expect(() => new ReadonlyDate(invalidDate)).toThrow(
144
- RangeError("Cannot instantiate ReadonlyDate via invalid date object"),
145
- );
146
-
147
- // Ideally we shouldn't need to worry about undefined values because the
148
- // constructor type signature will let you know when you got something
149
- // wrong
150
- const invalidNums: readonly number[] = [
151
- Number.NaN,
152
- Number.NEGATIVE_INFINITY,
153
- -Number.NEGATIVE_INFINITY,
154
- ];
155
- for (const i of invalidNums) {
156
- expect(() => new ReadonlyDate(i)).toThrow(
157
- RangeError("Cannot instantiate ReadonlyDate via invalid number(s)"),
158
- );
159
- }
160
-
161
- const invalidStrings: readonly string[] = [
162
- "blah",
163
- "2025-11-20 T13:59:19.545Z", // Extra space inserted
164
- ];
165
- for (const i of invalidStrings) {
166
- expect(() => new ReadonlyDate(i)).toThrow(
167
- RangeError("Cannot instantiate ReadonlyDate via invalid string"),
168
- );
169
- }
170
- });
171
- });
@@ -1,312 +0,0 @@
1
- /**
2
- * @file This comment is here to provide clarity on why proxy objects might
3
- * always be a dead end for this library, and document failed experiments.
4
- *
5
- * Readonly dates need to have a lot of interoperability with native dates
6
- * (pretty much every JavaScript library uses the built-in type). So, this code
7
- * originally defined them as a Proxy wrapper over native dates. The handler
8
- * intercepted all methods prefixed with `set` and turned them into no-ops.
9
- *
10
- * That got really close to working, but then development ran into a critical
11
- * limitation of the Proxy API. Basically, if the readonly date is defined with
12
- * a proxy, and you try to call Date.prototype.toISOString.call(readonlyDate),
13
- * that immediately blows up because the proxy itself is treated as the receiver
14
- * instead of the underlying native date.
15
- *
16
- * Vitest uses .call because it's the more airtight thing to do in most
17
- * situations, but proxy objects only have traps for .apply calls, not .call. So
18
- * there is no way in the language to intercept these calls and make sure
19
- * they're going to the right place. It is a hard, HARD limitation.
20
- *
21
- * The good news, though, is that having an extended class seems like the better
22
- * option, because it gives us the ability to define custom convenience methods
23
- * without breaking instanceof checks or breaking TypeScript assignability for
24
- * libraries that expect native dates. We just have to do a little bit of extra
25
- * work to fudge things for test runners.
26
- */
27
-
28
- /**
29
- * Any extra methods for readonly dates.
30
- */
31
- interface ReadonlyDateApi {
32
- /**
33
- * Converts a readonly date into a native (mutable) date.
34
- */
35
- toNativeDate(): Date;
36
- }
37
-
38
- /**
39
- * A readonly version of a Date object. To maximize compatibility with existing
40
- * libraries, all methods are the same as the native Date object at the type
41
- * level. But crucially, all methods prefixed with `set` have all mutation logic
42
- * removed.
43
- *
44
- * If you need a mutable version of the underlying date, ReadonlyDate exposes a
45
- * .toNativeDate method to do a runtime conversion to a native/mutable date.
46
- */
47
- export class ReadonlyDate extends Date implements ReadonlyDateApi {
48
- // Native dates support such a wide range of arguments (from 0 to 7), so
49
- // conditional types would be incredibly awkward here. Just using
50
- // constructor overloads instead
51
- constructor();
52
- constructor(initValue: number | string | Date);
53
- constructor(year: number, monthIndex: number);
54
- constructor(year: number, monthIndex: number, day: number);
55
- constructor(year: number, monthIndex: number, day: number, hours: number);
56
- constructor(
57
- year: number,
58
- monthIndex: number,
59
- day: number,
60
- hours: number,
61
- seconds: number,
62
- );
63
- constructor(
64
- year: number,
65
- monthIndex: number,
66
- day: number,
67
- hours: number,
68
- seconds: number,
69
- milliseconds: number,
70
- );
71
- constructor(
72
- initValue?: number | string | Date,
73
- monthIndex?: number,
74
- day?: number,
75
- hours?: number,
76
- minutes?: number,
77
- seconds?: number,
78
- milliseconds?: number,
79
- ) {
80
- /**
81
- * One problem with the native Date type is that they allow you to
82
- * produce invalid dates silently, and you won't find out until it's too
83
- * late. It's a lot like NaN for numbers.
84
- *
85
- * Taking some extra steps to make sure that they can't ever creep into
86
- * the library and break all the state modeling.
87
- *
88
- * Strings are still a problem, but that gets taken care of later in the
89
- * constructor.
90
- */
91
- const hasInvalidSourceDate =
92
- initValue instanceof Date && initValue.toString() === "Invalid Date";
93
- if (hasInvalidSourceDate) {
94
- throw new RangeError(
95
- "Cannot instantiate ReadonlyDate via invalid date object",
96
- );
97
- }
98
-
99
- /**
100
- * biome-ignore lint:complexity/noArguments -- We're going to be using
101
- * `arguments` a good bit because the native Date relies on the meta
102
- * parameter so much for runtime behavior
103
- */
104
- const hasInvalidNums = [...arguments].some((el) => {
105
- /**
106
- * You almost never see them in practice, but native dates do
107
- * support using negative AND fractional values for instantiation.
108
- * Negative values produce values before 1970.
109
- */
110
- return typeof el === "number" && !Number.isFinite(el);
111
- });
112
- if (hasInvalidNums) {
113
- throw new RangeError(
114
- "Cannot instantiate ReadonlyDate via invalid number(s)",
115
- );
116
- }
117
-
118
- /**
119
- * This guard clause looks incredibly silly, but we need to do this to
120
- * make sure that the readonly class works properly with Jest, Vitest,
121
- * and anything else that supports fake timers. Critically, it makes
122
- * this possible without introducing any extra runtime dependencies.
123
- *
124
- * Basically:
125
- * 1. We need to make sure that ReadonlyDate extends the Date prototype,
126
- * so that instanceof checks work correctly, and so that the class
127
- * can interop with all libraries that rely on vanilla Dates
128
- * 2. In ECMAScript, this linking happens right as the module is
129
- * imported
130
- * 3. Jest and Vitest will do some degree of hoisting before the
131
- * imports get evaluated, but most of the mock functionality happens
132
- * at runtime. useFakeTimers is NOT hoisted
133
- * 4. A Vitest test file might import the readonly class at some point
134
- * (directly or indirectly), which establishes the link
135
- * 5. useFakeTimers can then be called after imports, and that updates
136
- * the global scope so that when any FUTURE code references the
137
- * global Date object, the fake version is used instead
138
- * 6. But because the linking already happened before the call,
139
- * ReadonlyDate will still be bound to the original Date object
140
- * 7. When super is called (which is required when extending classes),
141
- * the original date object will be instantiated and then linked to
142
- * the readonly instance via the prototype chain
143
- * 8. None of this is a problem when you're instantiating the class by
144
- * passing it actual inputs, because then the date result will always
145
- * be deterministic. The problem happens when you make the date with
146
- * no arguments, because that causes a new date to be created with
147
- * the true system time, instead of the fake system time.
148
- * 9. So, to bridge the gap, we make a separate Date with `new Date()`
149
- * (after it's been turned into the fake version), and then use it to
150
- * overwrite the contents of the real date created with super
151
- */
152
- if (initValue === undefined) {
153
- super();
154
- const overrideForTestCorrectness = Date.now();
155
- super.setTime(overrideForTestCorrectness);
156
- return;
157
- }
158
-
159
- if (typeof initValue === "string") {
160
- super(initValue);
161
- if (super.toString() === "Invalid Date") {
162
- throw new RangeError(
163
- "Cannot instantiate ReadonlyDate via invalid string",
164
- );
165
- }
166
- return;
167
- }
168
-
169
- if (monthIndex === undefined) {
170
- super(initValue);
171
- return;
172
- }
173
- if (typeof initValue !== "number") {
174
- throw new TypeError(
175
- `Impossible case encountered: init value has type of '${typeof initValue}, but additional arguments were provided after the first`,
176
- );
177
- }
178
-
179
- /**
180
- * biome-ignore lint:complexity/noArguments -- Native dates are super
181
- * wonky, and they actually check arguments.length to define behavior
182
- * at runtime. We can't pass all the arguments in via a single call,
183
- * because then the constructor will create an invalid date the moment
184
- * it finds any single undefined value.
185
- */
186
- const argCount = arguments.length;
187
- switch (argCount) {
188
- case 2: {
189
- super(initValue, monthIndex);
190
- return;
191
- }
192
- case 3: {
193
- super(initValue, monthIndex, day);
194
- return;
195
- }
196
- case 4: {
197
- super(initValue, monthIndex, day, hours);
198
- return;
199
- }
200
- case 5: {
201
- super(initValue, monthIndex, day, hours, minutes);
202
- return;
203
- }
204
- case 6: {
205
- super(initValue, monthIndex, day, hours, minutes, seconds);
206
- return;
207
- }
208
- case 7: {
209
- super(
210
- initValue,
211
- monthIndex,
212
- day,
213
- hours,
214
- minutes,
215
- seconds,
216
- milliseconds,
217
- );
218
- return;
219
- }
220
- default: {
221
- throw new Error(
222
- `Cannot instantiate new Date with ${argCount} arguments`,
223
- );
224
- }
225
- }
226
- }
227
-
228
- toNativeDate(): Date {
229
- const time = super.getTime();
230
- return new Date(time);
231
- }
232
-
233
- ////////////////////////////////////////////////////////////////////////////
234
- // Start of custom set methods to shadow the ones from native dates. Note
235
- // that all set methods expect that the underlying timestamp be returned
236
- // afterwards, which always corresponds to Date.getTime.
237
- ////////////////////////////////////////////////////////////////////////////
238
-
239
- override setDate(_date: number): number {
240
- return super.getTime();
241
- }
242
-
243
- override setFullYear(_year: number, _month?: number, _date?: number): number {
244
- return super.getTime();
245
- }
246
-
247
- override setHours(
248
- _hours: number,
249
- _min?: number,
250
- _sec?: number,
251
- _ms?: number,
252
- ): number {
253
- return super.getTime();
254
- }
255
-
256
- override setMilliseconds(_ms: number): number {
257
- return super.getTime();
258
- }
259
-
260
- override setMinutes(_min: number, _sec?: number, _ms?: number): number {
261
- return super.getTime();
262
- }
263
-
264
- override setMonth(_month: number, _date?: number): number {
265
- return super.getTime();
266
- }
267
-
268
- override setSeconds(_sec: number, _ms?: number): number {
269
- return super.getTime();
270
- }
271
-
272
- override setTime(_time: number): number {
273
- return super.getTime();
274
- }
275
-
276
- override setUTCDate(_date: number): number {
277
- return super.getTime();
278
- }
279
-
280
- override setUTCFullYear(
281
- _year: number,
282
- _month?: number,
283
- _date?: number,
284
- ): number {
285
- return super.getTime();
286
- }
287
-
288
- override setUTCHours(
289
- _hours: number,
290
- _min?: number,
291
- _sec?: number,
292
- _ms?: number,
293
- ): number {
294
- return super.getTime();
295
- }
296
-
297
- override setUTCMilliseconds(_ms: number): number {
298
- return super.getTime();
299
- }
300
-
301
- override setUTCMinutes(_min: number, _sec?: number, _ms?: number): number {
302
- return super.getTime();
303
- }
304
-
305
- override setUTCMonth(_month: number, _date?: number): number {
306
- return super.getTime();
307
- }
308
-
309
- override setUTCSeconds(_sec: number, _ms?: number): number {
310
- return super.getTime();
311
- }
312
- }