@buenos-nachos/time-sync 0.5.5 → 0.6.1
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.
- package/dist/index.d.ts +1 -54
- package/dist/index.js +1 -175
- package/package.json +3 -6
- package/CHANGELOG.md +0 -97
- package/dist/index.cjs +0 -475
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -316
- package/dist/index.d.cts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/src/ReadonlyDate.test.ts +0 -171
- package/src/ReadonlyDate.ts +0 -312
- package/src/TimeSync.test.ts +0 -1232
- package/src/TimeSync.ts +0 -691
- package/src/index.ts +0 -12
package/src/ReadonlyDate.ts
DELETED
|
@@ -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
|
-
}
|