@buenos-nachos/time-sync 0.6.0 → 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 +0 -52
- package/dist/index.js +0 -173
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,30 +1,4 @@
|
|
|
1
1
|
//#region src/ReadonlyDate.d.ts
|
|
2
|
-
/**
|
|
3
|
-
* @file This comment is here to provide clarity on why proxy objects might
|
|
4
|
-
* always be a dead end for this library, and document failed experiments.
|
|
5
|
-
*
|
|
6
|
-
* Readonly dates need to have a lot of interoperability with native dates
|
|
7
|
-
* (pretty much every JavaScript library uses the built-in type). So, this code
|
|
8
|
-
* originally defined them as a Proxy wrapper over native dates. The handler
|
|
9
|
-
* intercepted all methods prefixed with `set` and turned them into no-ops.
|
|
10
|
-
*
|
|
11
|
-
* That got really close to working, but then development ran into a critical
|
|
12
|
-
* limitation of the Proxy API. Basically, if the readonly date is defined with
|
|
13
|
-
* a proxy, and you try to call Date.prototype.toISOString.call(readonlyDate),
|
|
14
|
-
* that immediately blows up because the proxy itself is treated as the receiver
|
|
15
|
-
* instead of the underlying native date.
|
|
16
|
-
*
|
|
17
|
-
* Vitest uses .call because it's the more airtight thing to do in most
|
|
18
|
-
* situations, but proxy objects only have traps for .apply calls, not .call. So
|
|
19
|
-
* there is no way in the language to intercept these calls and make sure
|
|
20
|
-
* they're going to the right place. It is a hard, HARD limitation.
|
|
21
|
-
*
|
|
22
|
-
* The good news, though, is that having an extended class seems like the better
|
|
23
|
-
* option, because it gives us the ability to define custom convenience methods
|
|
24
|
-
* without breaking instanceof checks or breaking TypeScript assignability for
|
|
25
|
-
* libraries that expect native dates. We just have to do a little bit of extra
|
|
26
|
-
* work to fudge things for test runners.
|
|
27
|
-
*/
|
|
28
2
|
/**
|
|
29
3
|
* Any extra methods for readonly dates.
|
|
30
4
|
*/
|
|
@@ -269,32 +243,6 @@ interface TimeSyncApi {
|
|
|
269
243
|
*/
|
|
270
244
|
clearAll: () => void;
|
|
271
245
|
}
|
|
272
|
-
/**
|
|
273
|
-
* One thing that was considered was giving TimeSync the ability to flip which
|
|
274
|
-
* kinds of dates it uses, and let it use native dates instead of readonly
|
|
275
|
-
* dates. We type readonly dates as native dates for better interoperability
|
|
276
|
-
* with pretty much every JavaScript library under the sun, but there is still a
|
|
277
|
-
* big difference in runtime behavior. There is a risk that blocking mutations
|
|
278
|
-
* could break some other library in other ways.
|
|
279
|
-
*
|
|
280
|
-
* That might be worth revisiting if we get user feedback, but right now, it
|
|
281
|
-
* seems like an incredibly bad idea.
|
|
282
|
-
*
|
|
283
|
-
* 1. Any single mutation has a risk of breaking the entire integrity of the
|
|
284
|
-
* system. If a consumer would try to mutate them, things SHOULD blow up by
|
|
285
|
-
* default.
|
|
286
|
-
* 2. Dates are a type of object that are far more read-heavy than write-heavy,
|
|
287
|
-
* so the risks of breaking are generally lower
|
|
288
|
-
* 3. If a user really needs a mutable version of the date, they can make a
|
|
289
|
-
* mutable copy first via `const mutable = readonlyDate.toNativeDate()`
|
|
290
|
-
*
|
|
291
|
-
* The one case when turning off the readonly behavior would be good would be
|
|
292
|
-
* if you're on a server that really needs to watch its garbage collection
|
|
293
|
-
* output, and you the overhead from the readonly date is causing too much
|
|
294
|
-
* pressure on resources. In that case, you could switch to native dates, but
|
|
295
|
-
* you'd still need a LOT of trigger discipline to avoid mutations, especially
|
|
296
|
-
* if you rely on outside libraries.
|
|
297
|
-
*/
|
|
298
246
|
/**
|
|
299
247
|
* TimeSync provides a centralized authority for working with time values in a
|
|
300
248
|
* more structured way. It ensures all dependents for the time values stay in
|
package/dist/index.js
CHANGED
|
@@ -12,47 +12,8 @@ var ReadonlyDate = class extends Date {
|
|
|
12
12
|
constructor(initValue, monthIndex, day, hours, minutes, seconds, milliseconds) {
|
|
13
13
|
if (initValue instanceof Date && initValue.toString() === "Invalid Date") throw new RangeError("Cannot instantiate ReadonlyDate via invalid date object");
|
|
14
14
|
if ([...arguments].some((el) => {
|
|
15
|
-
/**
|
|
16
|
-
* You almost never see them in practice, but native dates do
|
|
17
|
-
* support using negative AND fractional values for instantiation.
|
|
18
|
-
* Negative values produce values before 1970.
|
|
19
|
-
*/
|
|
20
15
|
return typeof el === "number" && !Number.isFinite(el);
|
|
21
16
|
})) throw new RangeError("Cannot instantiate ReadonlyDate via invalid number(s)");
|
|
22
|
-
/**
|
|
23
|
-
* This guard clause looks incredibly silly, but we need to do this to
|
|
24
|
-
* make sure that the readonly class works properly with Jest, Vitest,
|
|
25
|
-
* and anything else that supports fake timers. Critically, it makes
|
|
26
|
-
* this possible without introducing any extra runtime dependencies.
|
|
27
|
-
*
|
|
28
|
-
* Basically:
|
|
29
|
-
* 1. We need to make sure that ReadonlyDate extends the Date prototype,
|
|
30
|
-
* so that instanceof checks work correctly, and so that the class
|
|
31
|
-
* can interop with all libraries that rely on vanilla Dates
|
|
32
|
-
* 2. In ECMAScript, this linking happens right as the module is
|
|
33
|
-
* imported
|
|
34
|
-
* 3. Jest and Vitest will do some degree of hoisting before the
|
|
35
|
-
* imports get evaluated, but most of the mock functionality happens
|
|
36
|
-
* at runtime. useFakeTimers is NOT hoisted
|
|
37
|
-
* 4. A Vitest test file might import the readonly class at some point
|
|
38
|
-
* (directly or indirectly), which establishes the link
|
|
39
|
-
* 5. useFakeTimers can then be called after imports, and that updates
|
|
40
|
-
* the global scope so that when any FUTURE code references the
|
|
41
|
-
* global Date object, the fake version is used instead
|
|
42
|
-
* 6. But because the linking already happened before the call,
|
|
43
|
-
* ReadonlyDate will still be bound to the original Date object
|
|
44
|
-
* 7. When super is called (which is required when extending classes),
|
|
45
|
-
* the original date object will be instantiated and then linked to
|
|
46
|
-
* the readonly instance via the prototype chain
|
|
47
|
-
* 8. None of this is a problem when you're instantiating the class by
|
|
48
|
-
* passing it actual inputs, because then the date result will always
|
|
49
|
-
* be deterministic. The problem happens when you make the date with
|
|
50
|
-
* no arguments, because that causes a new date to be created with
|
|
51
|
-
* the true system time, instead of the fake system time.
|
|
52
|
-
* 9. So, to bridge the gap, we make a separate Date with `new Date()`
|
|
53
|
-
* (after it's been turned into the fake version), and then use it to
|
|
54
|
-
* overwrite the contents of the real date created with super
|
|
55
|
-
*/
|
|
56
17
|
if (initValue === void 0) {
|
|
57
18
|
super();
|
|
58
19
|
const overrideForTestCorrectness = Date.now();
|
|
@@ -69,13 +30,6 @@ var ReadonlyDate = class extends Date {
|
|
|
69
30
|
return;
|
|
70
31
|
}
|
|
71
32
|
if (typeof initValue !== "number") throw new TypeError(`Impossible case encountered: init value has type of '${typeof initValue}, but additional arguments were provided after the first`);
|
|
72
|
-
/**
|
|
73
|
-
* biome-ignore lint:complexity/noArguments -- Native dates are super
|
|
74
|
-
* wonky, and they actually check arguments.length to define behavior
|
|
75
|
-
* at runtime. We can't pass all the arguments in via a single call,
|
|
76
|
-
* because then the constructor will create an invalid date the moment
|
|
77
|
-
* it finds any single undefined value.
|
|
78
|
-
*/
|
|
79
33
|
const argCount = arguments.length;
|
|
80
34
|
switch (argCount) {
|
|
81
35
|
case 2:
|
|
@@ -164,12 +118,6 @@ const refreshRates = Object.freeze({
|
|
|
164
118
|
fiveMinutes: 300 * 1e3,
|
|
165
119
|
oneHour: 3600 * 1e3
|
|
166
120
|
});
|
|
167
|
-
/**
|
|
168
|
-
* Even though both the browser and the server are able to give monotonic times
|
|
169
|
-
* that are at least as precise as a nanosecond, we're using milliseconds for
|
|
170
|
-
* consistency with useInterval, which cannot be more precise than a
|
|
171
|
-
* millisecond.
|
|
172
|
-
*/
|
|
173
121
|
function getMonotonicTimeMs() {
|
|
174
122
|
if (typeof window === "undefined") {
|
|
175
123
|
const timeInNanoseconds = process.hrtime.bigint();
|
|
@@ -178,18 +126,6 @@ function getMonotonicTimeMs() {
|
|
|
178
126
|
const highResTimestamp = window.performance.now();
|
|
179
127
|
return Math.floor(highResTimestamp);
|
|
180
128
|
}
|
|
181
|
-
/**
|
|
182
|
-
* This function is just a convenience for us to sidestep some problems around
|
|
183
|
-
* TypeScript's LSP and Object.freeze. Because Object.freeze can accept any
|
|
184
|
-
* arbitrary type, it basically acts as a "type boundary" between the left and
|
|
185
|
-
* right sides of any snapshot assignments.
|
|
186
|
-
*
|
|
187
|
-
* That means that if you rename a property a a value that is passed to
|
|
188
|
-
* Object.freeze, the LSP can't auto-rename it, and you potentially get missing
|
|
189
|
-
* properties. This is a bit hokey, but because the function is defined strictly
|
|
190
|
-
* in terms of concrete snapshots, any value passed to this function won't have
|
|
191
|
-
* to worry about mismatches.
|
|
192
|
-
*/
|
|
193
129
|
function freezeSnapshot(snap) {
|
|
194
130
|
if (!Object.isFrozen(snap.config)) Object.freeze(snap.config);
|
|
195
131
|
if (!Object.isFrozen(snap)) Object.freeze(snap);
|
|
@@ -197,32 +133,6 @@ function freezeSnapshot(snap) {
|
|
|
197
133
|
}
|
|
198
134
|
const defaultMinimumRefreshIntervalMs = 200;
|
|
199
135
|
/**
|
|
200
|
-
* One thing that was considered was giving TimeSync the ability to flip which
|
|
201
|
-
* kinds of dates it uses, and let it use native dates instead of readonly
|
|
202
|
-
* dates. We type readonly dates as native dates for better interoperability
|
|
203
|
-
* with pretty much every JavaScript library under the sun, but there is still a
|
|
204
|
-
* big difference in runtime behavior. There is a risk that blocking mutations
|
|
205
|
-
* could break some other library in other ways.
|
|
206
|
-
*
|
|
207
|
-
* That might be worth revisiting if we get user feedback, but right now, it
|
|
208
|
-
* seems like an incredibly bad idea.
|
|
209
|
-
*
|
|
210
|
-
* 1. Any single mutation has a risk of breaking the entire integrity of the
|
|
211
|
-
* system. If a consumer would try to mutate them, things SHOULD blow up by
|
|
212
|
-
* default.
|
|
213
|
-
* 2. Dates are a type of object that are far more read-heavy than write-heavy,
|
|
214
|
-
* so the risks of breaking are generally lower
|
|
215
|
-
* 3. If a user really needs a mutable version of the date, they can make a
|
|
216
|
-
* mutable copy first via `const mutable = readonlyDate.toNativeDate()`
|
|
217
|
-
*
|
|
218
|
-
* The one case when turning off the readonly behavior would be good would be
|
|
219
|
-
* if you're on a server that really needs to watch its garbage collection
|
|
220
|
-
* output, and you the overhead from the readonly date is causing too much
|
|
221
|
-
* pressure on resources. In that case, you could switch to native dates, but
|
|
222
|
-
* you'd still need a LOT of trigger discipline to avoid mutations, especially
|
|
223
|
-
* if you rely on outside libraries.
|
|
224
|
-
*/
|
|
225
|
-
/**
|
|
226
136
|
* TimeSync provides a centralized authority for working with time values in a
|
|
227
137
|
* more structured way. It ensures all dependents for the time values stay in
|
|
228
138
|
* sync with each other.
|
|
@@ -232,67 +142,10 @@ const defaultMinimumRefreshIntervalMs = 200;
|
|
|
232
142
|
* some parts of the screen.)
|
|
233
143
|
*/
|
|
234
144
|
var TimeSync = class {
|
|
235
|
-
/**
|
|
236
|
-
* The monotonic time in milliseconds from when the TimeSync instance was
|
|
237
|
-
* first instantiated.
|
|
238
|
-
*/
|
|
239
145
|
#initializedAtMs;
|
|
240
|
-
/**
|
|
241
|
-
* Stores all refresh intervals actively associated with an onUpdate
|
|
242
|
-
* callback (along with their associated unsubscribe callbacks).
|
|
243
|
-
*
|
|
244
|
-
* Supports storing the exact same callback-interval pairs multiple times,
|
|
245
|
-
* in case multiple external systems need to subscribe with the exact same
|
|
246
|
-
* data concerns. Because the functions themselves are used as keys, that
|
|
247
|
-
* ensures that each callback will only be called once per update, no matter
|
|
248
|
-
* how subscribers use it.
|
|
249
|
-
*
|
|
250
|
-
* Each map value should stay sorted by refresh interval, in ascending
|
|
251
|
-
* order.
|
|
252
|
-
*
|
|
253
|
-
* ---
|
|
254
|
-
*
|
|
255
|
-
* This is a rare case where we actually REALLY need the readonly modifier
|
|
256
|
-
* to avoid infinite loops. JavaScript's iterator protocol is really great
|
|
257
|
-
* for making loops simple and type-safe, but because subscriptions have the
|
|
258
|
-
* ability to add more subscriptions, we need to make an immutable version
|
|
259
|
-
* of each array at some point to make sure that we're not iterating through
|
|
260
|
-
* values forever
|
|
261
|
-
*
|
|
262
|
-
* We can choose to do that at one of two points:
|
|
263
|
-
* 1. When adding a new subscription
|
|
264
|
-
* 2. When dispatching a new round of updates
|
|
265
|
-
*
|
|
266
|
-
* Because this library assumes that dispatches will be much more common
|
|
267
|
-
* than new subscriptions (a single subscription that subscribes for one
|
|
268
|
-
* second will receive 360 updates in five minutes), operations should be
|
|
269
|
-
* done to optimize that use case. So we should move the immutability costs
|
|
270
|
-
* to the subscribe and unsubscribe operations.
|
|
271
|
-
*/
|
|
272
146
|
#subscriptions;
|
|
273
|
-
/**
|
|
274
|
-
* The latest public snapshot of TimeSync's internal state. The snapshot
|
|
275
|
-
* should always be treated as an immutable value.
|
|
276
|
-
*/
|
|
277
147
|
#latestSnapshot;
|
|
278
|
-
/**
|
|
279
|
-
* A cached version of the fastest interval currently registered with
|
|
280
|
-
* TimeSync. Should always be derived from #subscriptions
|
|
281
|
-
*/
|
|
282
148
|
#fastestRefreshInterval;
|
|
283
|
-
/**
|
|
284
|
-
* Used for both its intended purpose (creating interval), but also as a
|
|
285
|
-
* janky version of setTimeout. Also, all versions of setInterval are
|
|
286
|
-
* monotonic, so we don't have to do anything special for it.
|
|
287
|
-
*
|
|
288
|
-
* There are a few times when we need timeout-like logic, but if we use
|
|
289
|
-
* setInterval for everything, we have fewer IDs to juggle, and less risk of
|
|
290
|
-
* things getting out of sync.
|
|
291
|
-
*
|
|
292
|
-
* Type defined like this to support client and server behavior. Node.js
|
|
293
|
-
* uses its own custom timeout type, but Deno, Bun, and the browser all use
|
|
294
|
-
* the number type.
|
|
295
|
-
*/
|
|
296
149
|
#intervalId;
|
|
297
150
|
constructor(options) {
|
|
298
151
|
const { initialDate, freezeUpdates = false, allowDuplicateOnUpdateCalls = true, minimumRefreshIntervalMs = defaultMinimumRefreshIntervalMs } = options ?? {};
|
|
@@ -319,24 +172,6 @@ var TimeSync = class {
|
|
|
319
172
|
#notifyAllSubscriptions() {
|
|
320
173
|
const { date, config } = this.#latestSnapshot;
|
|
321
174
|
if (config.freezeUpdates || this.#subscriptions.size === 0 || this.#fastestRefreshInterval === Number.POSITIVE_INFINITY) return;
|
|
322
|
-
/**
|
|
323
|
-
* Two things:
|
|
324
|
-
* 1. Even though the context arrays are defined as readonly (which
|
|
325
|
-
* removes on the worst edge cases during dispatching), the
|
|
326
|
-
* subscriptions map itself is still mutable, so there are a few edge
|
|
327
|
-
* cases we need to deal with. While the risk of infinite loops should
|
|
328
|
-
* be much lower, there's still the risk that an onUpdate callback could
|
|
329
|
-
* add a subscriber for an interval that wasn't registered before, which
|
|
330
|
-
* the iterator protocol will pick up. Need to make a local,
|
|
331
|
-
* fixed-length copy of the map entries before starting iteration. Any
|
|
332
|
-
* subscriptions added during update will just have to wait until the
|
|
333
|
-
* next round of updates.
|
|
334
|
-
*
|
|
335
|
-
* 2. The trade off of the serialization is that we do lose the ability
|
|
336
|
-
* to auto-break the loop if one of the subscribers ends up resetting
|
|
337
|
-
* all state, because we'll still have local copies of entries. We need
|
|
338
|
-
* to check on each iteration to see if we should continue.
|
|
339
|
-
*/
|
|
340
175
|
const subsBeforeUpdate = this.#subscriptions;
|
|
341
176
|
const localEntries = Array.from(subsBeforeUpdate);
|
|
342
177
|
outer: for (const [onUpdate, subs] of localEntries) for (const ctx of subs) {
|
|
@@ -345,14 +180,6 @@ var TimeSync = class {
|
|
|
345
180
|
if (!config.allowDuplicateOnUpdateCalls) continue outer;
|
|
346
181
|
}
|
|
347
182
|
}
|
|
348
|
-
/**
|
|
349
|
-
* The logic that should happen at each step in TimeSync's active interval.
|
|
350
|
-
*
|
|
351
|
-
* Defined as an arrow function so that we can just pass it directly to
|
|
352
|
-
* setInterval without needing to make a new wrapper function each time. We
|
|
353
|
-
* don't have many situations where we can lose the `this` context, but this
|
|
354
|
-
* is one of them.
|
|
355
|
-
*/
|
|
356
183
|
#onTick = () => {
|
|
357
184
|
const { config } = this.#latestSnapshot;
|
|
358
185
|
if (config.freezeUpdates) {
|