@buenos-nachos/time-sync 0.5.4 → 0.5.5
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/CHANGELOG.md +6 -0
- package/dist/index.cjs +475 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +316 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +316 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +472 -0
- package/dist/index.js.map +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/ReadonlyDate.ts
|
|
3
|
+
/**
|
|
4
|
+
* A readonly version of a Date object. To maximize compatibility with existing
|
|
5
|
+
* libraries, all methods are the same as the native Date object at the type
|
|
6
|
+
* level. But crucially, all methods prefixed with `set` have all mutation logic
|
|
7
|
+
* removed.
|
|
8
|
+
*
|
|
9
|
+
* If you need a mutable version of the underlying date, ReadonlyDate exposes a
|
|
10
|
+
* .toNativeDate method to do a runtime conversion to a native/mutable date.
|
|
11
|
+
*/
|
|
12
|
+
var ReadonlyDate = class extends Date {
|
|
13
|
+
constructor(initValue, monthIndex, day, hours, minutes, seconds, milliseconds) {
|
|
14
|
+
if (initValue instanceof Date && initValue.toString() === "Invalid Date") throw new RangeError("Cannot instantiate ReadonlyDate via invalid date object");
|
|
15
|
+
if ([...arguments].some((el) => {
|
|
16
|
+
/**
|
|
17
|
+
* You almost never see them in practice, but native dates do
|
|
18
|
+
* support using negative AND fractional values for instantiation.
|
|
19
|
+
* Negative values produce values before 1970.
|
|
20
|
+
*/
|
|
21
|
+
return typeof el === "number" && !Number.isFinite(el);
|
|
22
|
+
})) throw new RangeError("Cannot instantiate ReadonlyDate via invalid number(s)");
|
|
23
|
+
/**
|
|
24
|
+
* This guard clause looks incredibly silly, but we need to do this to
|
|
25
|
+
* make sure that the readonly class works properly with Jest, Vitest,
|
|
26
|
+
* and anything else that supports fake timers. Critically, it makes
|
|
27
|
+
* this possible without introducing any extra runtime dependencies.
|
|
28
|
+
*
|
|
29
|
+
* Basically:
|
|
30
|
+
* 1. We need to make sure that ReadonlyDate extends the Date prototype,
|
|
31
|
+
* so that instanceof checks work correctly, and so that the class
|
|
32
|
+
* can interop with all libraries that rely on vanilla Dates
|
|
33
|
+
* 2. In ECMAScript, this linking happens right as the module is
|
|
34
|
+
* imported
|
|
35
|
+
* 3. Jest and Vitest will do some degree of hoisting before the
|
|
36
|
+
* imports get evaluated, but most of the mock functionality happens
|
|
37
|
+
* at runtime. useFakeTimers is NOT hoisted
|
|
38
|
+
* 4. A Vitest test file might import the readonly class at some point
|
|
39
|
+
* (directly or indirectly), which establishes the link
|
|
40
|
+
* 5. useFakeTimers can then be called after imports, and that updates
|
|
41
|
+
* the global scope so that when any FUTURE code references the
|
|
42
|
+
* global Date object, the fake version is used instead
|
|
43
|
+
* 6. But because the linking already happened before the call,
|
|
44
|
+
* ReadonlyDate will still be bound to the original Date object
|
|
45
|
+
* 7. When super is called (which is required when extending classes),
|
|
46
|
+
* the original date object will be instantiated and then linked to
|
|
47
|
+
* the readonly instance via the prototype chain
|
|
48
|
+
* 8. None of this is a problem when you're instantiating the class by
|
|
49
|
+
* passing it actual inputs, because then the date result will always
|
|
50
|
+
* be deterministic. The problem happens when you make the date with
|
|
51
|
+
* no arguments, because that causes a new date to be created with
|
|
52
|
+
* the true system time, instead of the fake system time.
|
|
53
|
+
* 9. So, to bridge the gap, we make a separate Date with `new Date()`
|
|
54
|
+
* (after it's been turned into the fake version), and then use it to
|
|
55
|
+
* overwrite the contents of the real date created with super
|
|
56
|
+
*/
|
|
57
|
+
if (initValue === void 0) {
|
|
58
|
+
super();
|
|
59
|
+
const overrideForTestCorrectness = Date.now();
|
|
60
|
+
super.setTime(overrideForTestCorrectness);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (typeof initValue === "string") {
|
|
64
|
+
super(initValue);
|
|
65
|
+
if (super.toString() === "Invalid Date") throw new RangeError("Cannot instantiate ReadonlyDate via invalid string");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (monthIndex === void 0) {
|
|
69
|
+
super(initValue);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
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`);
|
|
73
|
+
/**
|
|
74
|
+
* biome-ignore lint:complexity/noArguments -- Native dates are super
|
|
75
|
+
* wonky, and they actually check arguments.length to define behavior
|
|
76
|
+
* at runtime. We can't pass all the arguments in via a single call,
|
|
77
|
+
* because then the constructor will create an invalid date the moment
|
|
78
|
+
* it finds any single undefined value.
|
|
79
|
+
*/
|
|
80
|
+
const argCount = arguments.length;
|
|
81
|
+
switch (argCount) {
|
|
82
|
+
case 2:
|
|
83
|
+
super(initValue, monthIndex);
|
|
84
|
+
return;
|
|
85
|
+
case 3:
|
|
86
|
+
super(initValue, monthIndex, day);
|
|
87
|
+
return;
|
|
88
|
+
case 4:
|
|
89
|
+
super(initValue, monthIndex, day, hours);
|
|
90
|
+
return;
|
|
91
|
+
case 5:
|
|
92
|
+
super(initValue, monthIndex, day, hours, minutes);
|
|
93
|
+
return;
|
|
94
|
+
case 6:
|
|
95
|
+
super(initValue, monthIndex, day, hours, minutes, seconds);
|
|
96
|
+
return;
|
|
97
|
+
case 7:
|
|
98
|
+
super(initValue, monthIndex, day, hours, minutes, seconds, milliseconds);
|
|
99
|
+
return;
|
|
100
|
+
default: throw new Error(`Cannot instantiate new Date with ${argCount} arguments`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
toNativeDate() {
|
|
104
|
+
const time = super.getTime();
|
|
105
|
+
return new Date(time);
|
|
106
|
+
}
|
|
107
|
+
setDate(_date) {
|
|
108
|
+
return super.getTime();
|
|
109
|
+
}
|
|
110
|
+
setFullYear(_year, _month, _date) {
|
|
111
|
+
return super.getTime();
|
|
112
|
+
}
|
|
113
|
+
setHours(_hours, _min, _sec, _ms) {
|
|
114
|
+
return super.getTime();
|
|
115
|
+
}
|
|
116
|
+
setMilliseconds(_ms) {
|
|
117
|
+
return super.getTime();
|
|
118
|
+
}
|
|
119
|
+
setMinutes(_min, _sec, _ms) {
|
|
120
|
+
return super.getTime();
|
|
121
|
+
}
|
|
122
|
+
setMonth(_month, _date) {
|
|
123
|
+
return super.getTime();
|
|
124
|
+
}
|
|
125
|
+
setSeconds(_sec, _ms) {
|
|
126
|
+
return super.getTime();
|
|
127
|
+
}
|
|
128
|
+
setTime(_time) {
|
|
129
|
+
return super.getTime();
|
|
130
|
+
}
|
|
131
|
+
setUTCDate(_date) {
|
|
132
|
+
return super.getTime();
|
|
133
|
+
}
|
|
134
|
+
setUTCFullYear(_year, _month, _date) {
|
|
135
|
+
return super.getTime();
|
|
136
|
+
}
|
|
137
|
+
setUTCHours(_hours, _min, _sec, _ms) {
|
|
138
|
+
return super.getTime();
|
|
139
|
+
}
|
|
140
|
+
setUTCMilliseconds(_ms) {
|
|
141
|
+
return super.getTime();
|
|
142
|
+
}
|
|
143
|
+
setUTCMinutes(_min, _sec, _ms) {
|
|
144
|
+
return super.getTime();
|
|
145
|
+
}
|
|
146
|
+
setUTCMonth(_month, _date) {
|
|
147
|
+
return super.getTime();
|
|
148
|
+
}
|
|
149
|
+
setUTCSeconds(_sec, _ms) {
|
|
150
|
+
return super.getTime();
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region src/TimeSync.ts
|
|
156
|
+
/**
|
|
157
|
+
* A collection of commonly-needed intervals (all defined in milliseconds).
|
|
158
|
+
*/
|
|
159
|
+
const refreshRates = Object.freeze({
|
|
160
|
+
idle: Number.POSITIVE_INFINITY,
|
|
161
|
+
halfSecond: 500,
|
|
162
|
+
oneSecond: 1e3,
|
|
163
|
+
thirtySeconds: 3e4,
|
|
164
|
+
oneMinute: 60 * 1e3,
|
|
165
|
+
fiveMinutes: 300 * 1e3,
|
|
166
|
+
oneHour: 3600 * 1e3
|
|
167
|
+
});
|
|
168
|
+
/**
|
|
169
|
+
* Even though both the browser and the server are able to give monotonic times
|
|
170
|
+
* that are at least as precise as a nanosecond, we're using milliseconds for
|
|
171
|
+
* consistency with useInterval, which cannot be more precise than a
|
|
172
|
+
* millisecond.
|
|
173
|
+
*/
|
|
174
|
+
function getMonotonicTimeMs() {
|
|
175
|
+
if (typeof window === "undefined") {
|
|
176
|
+
const timeInNanoseconds = process.hrtime.bigint();
|
|
177
|
+
return Number(timeInNanoseconds / 1000n);
|
|
178
|
+
}
|
|
179
|
+
const highResTimestamp = window.performance.now();
|
|
180
|
+
return Math.floor(highResTimestamp);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* This function is just a convenience for us to sidestep some problems around
|
|
184
|
+
* TypeScript's LSP and Object.freeze. Because Object.freeze can accept any
|
|
185
|
+
* arbitrary type, it basically acts as a "type boundary" between the left and
|
|
186
|
+
* right sides of any snapshot assignments.
|
|
187
|
+
*
|
|
188
|
+
* That means that if you rename a property a a value that is passed to
|
|
189
|
+
* Object.freeze, the LSP can't auto-rename it, and you potentially get missing
|
|
190
|
+
* properties. This is a bit hokey, but because the function is defined strictly
|
|
191
|
+
* in terms of concrete snapshots, any value passed to this function won't have
|
|
192
|
+
* to worry about mismatches.
|
|
193
|
+
*/
|
|
194
|
+
function freezeSnapshot(snap) {
|
|
195
|
+
if (!Object.isFrozen(snap.config)) Object.freeze(snap.config);
|
|
196
|
+
if (!Object.isFrozen(snap)) Object.freeze(snap);
|
|
197
|
+
return snap;
|
|
198
|
+
}
|
|
199
|
+
const defaultMinimumRefreshIntervalMs = 200;
|
|
200
|
+
/**
|
|
201
|
+
* One thing that was considered was giving TimeSync the ability to flip which
|
|
202
|
+
* kinds of dates it uses, and let it use native dates instead of readonly
|
|
203
|
+
* dates. We type readonly dates as native dates for better interoperability
|
|
204
|
+
* with pretty much every JavaScript library under the sun, but there is still a
|
|
205
|
+
* big difference in runtime behavior. There is a risk that blocking mutations
|
|
206
|
+
* could break some other library in other ways.
|
|
207
|
+
*
|
|
208
|
+
* That might be worth revisiting if we get user feedback, but right now, it
|
|
209
|
+
* seems like an incredibly bad idea.
|
|
210
|
+
*
|
|
211
|
+
* 1. Any single mutation has a risk of breaking the entire integrity of the
|
|
212
|
+
* system. If a consumer would try to mutate them, things SHOULD blow up by
|
|
213
|
+
* default.
|
|
214
|
+
* 2. Dates are a type of object that are far more read-heavy than write-heavy,
|
|
215
|
+
* so the risks of breaking are generally lower
|
|
216
|
+
* 3. If a user really needs a mutable version of the date, they can make a
|
|
217
|
+
* mutable copy first via `const mutable = readonlyDate.toNativeDate()`
|
|
218
|
+
*
|
|
219
|
+
* The one case when turning off the readonly behavior would be good would be
|
|
220
|
+
* if you're on a server that really needs to watch its garbage collection
|
|
221
|
+
* output, and you the overhead from the readonly date is causing too much
|
|
222
|
+
* pressure on resources. In that case, you could switch to native dates, but
|
|
223
|
+
* you'd still need a LOT of trigger discipline to avoid mutations, especially
|
|
224
|
+
* if you rely on outside libraries.
|
|
225
|
+
*/
|
|
226
|
+
/**
|
|
227
|
+
* TimeSync provides a centralized authority for working with time values in a
|
|
228
|
+
* more structured way. It ensures all dependents for the time values stay in
|
|
229
|
+
* sync with each other.
|
|
230
|
+
*
|
|
231
|
+
* (e.g., In a React codebase, you want multiple components that rely on time
|
|
232
|
+
* values to update together, to avoid screen tearing and stale data for only
|
|
233
|
+
* some parts of the screen.)
|
|
234
|
+
*/
|
|
235
|
+
var TimeSync = class {
|
|
236
|
+
/**
|
|
237
|
+
* The monotonic time in milliseconds from when the TimeSync instance was
|
|
238
|
+
* first instantiated.
|
|
239
|
+
*/
|
|
240
|
+
#initializedAtMs;
|
|
241
|
+
/**
|
|
242
|
+
* Stores all refresh intervals actively associated with an onUpdate
|
|
243
|
+
* callback (along with their associated unsubscribe callbacks).
|
|
244
|
+
*
|
|
245
|
+
* Supports storing the exact same callback-interval pairs multiple times,
|
|
246
|
+
* in case multiple external systems need to subscribe with the exact same
|
|
247
|
+
* data concerns. Because the functions themselves are used as keys, that
|
|
248
|
+
* ensures that each callback will only be called once per update, no matter
|
|
249
|
+
* how subscribers use it.
|
|
250
|
+
*
|
|
251
|
+
* Each map value should stay sorted by refresh interval, in ascending
|
|
252
|
+
* order.
|
|
253
|
+
*
|
|
254
|
+
* ---
|
|
255
|
+
*
|
|
256
|
+
* This is a rare case where we actually REALLY need the readonly modifier
|
|
257
|
+
* to avoid infinite loops. JavaScript's iterator protocol is really great
|
|
258
|
+
* for making loops simple and type-safe, but because subscriptions have the
|
|
259
|
+
* ability to add more subscriptions, we need to make an immutable version
|
|
260
|
+
* of each array at some point to make sure that we're not iterating through
|
|
261
|
+
* values forever
|
|
262
|
+
*
|
|
263
|
+
* We can choose to do that at one of two points:
|
|
264
|
+
* 1. When adding a new subscription
|
|
265
|
+
* 2. When dispatching a new round of updates
|
|
266
|
+
*
|
|
267
|
+
* Because this library assumes that dispatches will be much more common
|
|
268
|
+
* than new subscriptions (a single subscription that subscribes for one
|
|
269
|
+
* second will receive 360 updates in five minutes), operations should be
|
|
270
|
+
* done to optimize that use case. So we should move the immutability costs
|
|
271
|
+
* to the subscribe and unsubscribe operations.
|
|
272
|
+
*/
|
|
273
|
+
#subscriptions;
|
|
274
|
+
/**
|
|
275
|
+
* The latest public snapshot of TimeSync's internal state. The snapshot
|
|
276
|
+
* should always be treated as an immutable value.
|
|
277
|
+
*/
|
|
278
|
+
#latestSnapshot;
|
|
279
|
+
/**
|
|
280
|
+
* A cached version of the fastest interval currently registered with
|
|
281
|
+
* TimeSync. Should always be derived from #subscriptions
|
|
282
|
+
*/
|
|
283
|
+
#fastestRefreshInterval;
|
|
284
|
+
/**
|
|
285
|
+
* Used for both its intended purpose (creating interval), but also as a
|
|
286
|
+
* janky version of setTimeout. Also, all versions of setInterval are
|
|
287
|
+
* monotonic, so we don't have to do anything special for it.
|
|
288
|
+
*
|
|
289
|
+
* There are a few times when we need timeout-like logic, but if we use
|
|
290
|
+
* setInterval for everything, we have fewer IDs to juggle, and less risk of
|
|
291
|
+
* things getting out of sync.
|
|
292
|
+
*
|
|
293
|
+
* Type defined like this to support client and server behavior. Node.js
|
|
294
|
+
* uses its own custom timeout type, but Deno, Bun, and the browser all use
|
|
295
|
+
* the number type.
|
|
296
|
+
*/
|
|
297
|
+
#intervalId;
|
|
298
|
+
constructor(options) {
|
|
299
|
+
const { initialDate, freezeUpdates = false, allowDuplicateOnUpdateCalls = true, minimumRefreshIntervalMs = defaultMinimumRefreshIntervalMs } = options ?? {};
|
|
300
|
+
if (!(Number.isInteger(minimumRefreshIntervalMs) && minimumRefreshIntervalMs > 0)) throw new RangeError(`Minimum refresh interval must be a positive integer (received ${minimumRefreshIntervalMs} ms)`);
|
|
301
|
+
this.#subscriptions = /* @__PURE__ */ new Map();
|
|
302
|
+
this.#fastestRefreshInterval = Number.POSITIVE_INFINITY;
|
|
303
|
+
this.#intervalId = void 0;
|
|
304
|
+
this.#initializedAtMs = getMonotonicTimeMs();
|
|
305
|
+
let date;
|
|
306
|
+
if (initialDate instanceof ReadonlyDate) date = initialDate;
|
|
307
|
+
else if (initialDate instanceof Date) date = new ReadonlyDate(initialDate);
|
|
308
|
+
else date = new ReadonlyDate();
|
|
309
|
+
this.#latestSnapshot = freezeSnapshot({
|
|
310
|
+
date,
|
|
311
|
+
subscriberCount: 0,
|
|
312
|
+
lastUpdatedAtMs: null,
|
|
313
|
+
config: {
|
|
314
|
+
freezeUpdates,
|
|
315
|
+
minimumRefreshIntervalMs,
|
|
316
|
+
allowDuplicateOnUpdateCalls
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
#notifyAllSubscriptions() {
|
|
321
|
+
const { date, config } = this.#latestSnapshot;
|
|
322
|
+
if (config.freezeUpdates || this.#subscriptions.size === 0 || this.#fastestRefreshInterval === Number.POSITIVE_INFINITY) return;
|
|
323
|
+
/**
|
|
324
|
+
* Two things:
|
|
325
|
+
* 1. Even though the context arrays are defined as readonly (which
|
|
326
|
+
* removes on the worst edge cases during dispatching), the
|
|
327
|
+
* subscriptions map itself is still mutable, so there are a few edge
|
|
328
|
+
* cases we need to deal with. While the risk of infinite loops should
|
|
329
|
+
* be much lower, there's still the risk that an onUpdate callback could
|
|
330
|
+
* add a subscriber for an interval that wasn't registered before, which
|
|
331
|
+
* the iterator protocol will pick up. Need to make a local,
|
|
332
|
+
* fixed-length copy of the map entries before starting iteration. Any
|
|
333
|
+
* subscriptions added during update will just have to wait until the
|
|
334
|
+
* next round of updates.
|
|
335
|
+
*
|
|
336
|
+
* 2. The trade off of the serialization is that we do lose the ability
|
|
337
|
+
* to auto-break the loop if one of the subscribers ends up resetting
|
|
338
|
+
* all state, because we'll still have local copies of entries. We need
|
|
339
|
+
* to check on each iteration to see if we should continue.
|
|
340
|
+
*/
|
|
341
|
+
const subsBeforeUpdate = this.#subscriptions;
|
|
342
|
+
const localEntries = Array.from(subsBeforeUpdate);
|
|
343
|
+
outer: for (const [onUpdate, subs] of localEntries) for (const ctx of subs) {
|
|
344
|
+
if (subsBeforeUpdate.size === 0) break outer;
|
|
345
|
+
onUpdate(date, ctx);
|
|
346
|
+
if (!config.allowDuplicateOnUpdateCalls) continue outer;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* The logic that should happen at each step in TimeSync's active interval.
|
|
351
|
+
*
|
|
352
|
+
* Defined as an arrow function so that we can just pass it directly to
|
|
353
|
+
* setInterval without needing to make a new wrapper function each time. We
|
|
354
|
+
* don't have many situations where we can lose the `this` context, but this
|
|
355
|
+
* is one of them.
|
|
356
|
+
*/
|
|
357
|
+
#onTick = () => {
|
|
358
|
+
const { config } = this.#latestSnapshot;
|
|
359
|
+
if (config.freezeUpdates) {
|
|
360
|
+
clearInterval(this.#intervalId);
|
|
361
|
+
this.#intervalId = void 0;
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
this.#latestSnapshot = freezeSnapshot({
|
|
365
|
+
...this.#latestSnapshot,
|
|
366
|
+
date: new ReadonlyDate(),
|
|
367
|
+
lastUpdatedAtMs: getMonotonicTimeMs() - this.#initializedAtMs
|
|
368
|
+
});
|
|
369
|
+
this.#notifyAllSubscriptions();
|
|
370
|
+
};
|
|
371
|
+
#onFastestIntervalChange() {
|
|
372
|
+
const fastest = this.#fastestRefreshInterval;
|
|
373
|
+
const { lastUpdatedAtMs, config } = this.#latestSnapshot;
|
|
374
|
+
if (config.freezeUpdates || this.#subscriptions.size === 0 || fastest === Number.POSITIVE_INFINITY) {
|
|
375
|
+
clearInterval(this.#intervalId);
|
|
376
|
+
this.#intervalId = void 0;
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const timeBeforeNextUpdate = fastest - (getMonotonicTimeMs() - (lastUpdatedAtMs ?? this.#initializedAtMs));
|
|
380
|
+
clearInterval(this.#intervalId);
|
|
381
|
+
if (timeBeforeNextUpdate <= 0) {
|
|
382
|
+
this.#onTick();
|
|
383
|
+
this.#intervalId = setInterval(this.#onTick, fastest);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (timeBeforeNextUpdate === fastest) {
|
|
387
|
+
this.#intervalId = setInterval(this.#onTick, timeBeforeNextUpdate);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
this.#intervalId = setInterval(() => {
|
|
391
|
+
clearInterval(this.#intervalId);
|
|
392
|
+
this.#intervalId = setInterval(this.#onTick, fastest);
|
|
393
|
+
this.#onTick();
|
|
394
|
+
}, timeBeforeNextUpdate);
|
|
395
|
+
}
|
|
396
|
+
#updateFastestInterval() {
|
|
397
|
+
const { config } = this.#latestSnapshot;
|
|
398
|
+
if (config.freezeUpdates) {
|
|
399
|
+
this.#fastestRefreshInterval = Number.POSITIVE_INFINITY;
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const prevFastest = this.#fastestRefreshInterval;
|
|
403
|
+
let newFastest = Number.POSITIVE_INFINITY;
|
|
404
|
+
for (const contexts of this.#subscriptions.values()) {
|
|
405
|
+
const subFastest = contexts[0]?.refreshIntervalMs ?? Number.POSITIVE_INFINITY;
|
|
406
|
+
if (subFastest < newFastest) newFastest = subFastest;
|
|
407
|
+
}
|
|
408
|
+
this.#fastestRefreshInterval = newFastest;
|
|
409
|
+
if (prevFastest !== newFastest) this.#onFastestIntervalChange();
|
|
410
|
+
}
|
|
411
|
+
subscribe(options) {
|
|
412
|
+
const { targetRefreshIntervalMs, onUpdate } = options;
|
|
413
|
+
const { minimumRefreshIntervalMs } = this.#latestSnapshot.config;
|
|
414
|
+
if (!(targetRefreshIntervalMs === Number.POSITIVE_INFINITY || Number.isInteger(targetRefreshIntervalMs) && targetRefreshIntervalMs > 0)) throw new Error(`Target refresh interval must be positive infinity or a positive integer (received ${targetRefreshIntervalMs} ms)`);
|
|
415
|
+
const subsOnSetup = this.#subscriptions;
|
|
416
|
+
let subscribed = true;
|
|
417
|
+
const ctx = {
|
|
418
|
+
timeSync: this,
|
|
419
|
+
registeredAtMs: getMonotonicTimeMs() - this.#initializedAtMs,
|
|
420
|
+
refreshIntervalMs: Math.max(minimumRefreshIntervalMs, targetRefreshIntervalMs),
|
|
421
|
+
unsubscribe: () => {
|
|
422
|
+
try {
|
|
423
|
+
if (!subscribed || this.#subscriptions !== subsOnSetup) return;
|
|
424
|
+
const contexts = subsOnSetup.get(onUpdate);
|
|
425
|
+
if (contexts === void 0) return;
|
|
426
|
+
const filtered = contexts.filter((c) => c.unsubscribe !== ctx.unsubscribe);
|
|
427
|
+
if (filtered.length === contexts.length) return;
|
|
428
|
+
const dropped = Math.max(0, this.#latestSnapshot.subscriberCount - 1);
|
|
429
|
+
this.#latestSnapshot = freezeSnapshot({
|
|
430
|
+
...this.#latestSnapshot,
|
|
431
|
+
subscriberCount: dropped
|
|
432
|
+
});
|
|
433
|
+
if (filtered.length > 0) subsOnSetup.set(onUpdate, filtered);
|
|
434
|
+
else subsOnSetup.delete(onUpdate);
|
|
435
|
+
this.#updateFastestInterval();
|
|
436
|
+
} finally {
|
|
437
|
+
subscribed = false;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
Object.freeze(ctx);
|
|
442
|
+
let newContexts;
|
|
443
|
+
const prevContexts = subsOnSetup.get(onUpdate);
|
|
444
|
+
if (prevContexts !== void 0) newContexts = [...prevContexts, ctx];
|
|
445
|
+
else newContexts = [ctx];
|
|
446
|
+
subsOnSetup.set(onUpdate, newContexts);
|
|
447
|
+
newContexts.sort((c1, c2) => c1.refreshIntervalMs - c2.refreshIntervalMs);
|
|
448
|
+
this.#latestSnapshot = freezeSnapshot({
|
|
449
|
+
...this.#latestSnapshot,
|
|
450
|
+
subscriberCount: this.#latestSnapshot.subscriberCount + 1
|
|
451
|
+
});
|
|
452
|
+
this.#updateFastestInterval();
|
|
453
|
+
return ctx.unsubscribe;
|
|
454
|
+
}
|
|
455
|
+
getStateSnapshot() {
|
|
456
|
+
return this.#latestSnapshot;
|
|
457
|
+
}
|
|
458
|
+
clearAll() {
|
|
459
|
+
clearInterval(this.#intervalId);
|
|
460
|
+
this.#intervalId = void 0;
|
|
461
|
+
this.#fastestRefreshInterval = Number.POSITIVE_INFINITY;
|
|
462
|
+
this.#subscriptions.clear();
|
|
463
|
+
this.#subscriptions = /* @__PURE__ */ new Map();
|
|
464
|
+
this.#latestSnapshot = freezeSnapshot({
|
|
465
|
+
...this.#latestSnapshot,
|
|
466
|
+
subscriberCount: 0
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
//#endregion
|
|
472
|
+
exports.ReadonlyDate = ReadonlyDate;
|
|
473
|
+
exports.TimeSync = TimeSync;
|
|
474
|
+
exports.refreshRates = refreshRates;
|
|
475
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["#initializedAtMs","#subscriptions","#fastestRefreshInterval","#intervalId","date: ReadonlyDate","#latestSnapshot","#onTick","#notifyAllSubscriptions","#onFastestIntervalChange","ctx: SubscriptionContext","#updateFastestInterval","newContexts: SubscriptionContext[]"],"sources":["../src/ReadonlyDate.ts","../src/TimeSync.ts"],"sourcesContent":["/**\n * @file This comment is here to provide clarity on why proxy objects might\n * always be a dead end for this library, and document failed experiments.\n *\n * Readonly dates need to have a lot of interoperability with native dates\n * (pretty much every JavaScript library uses the built-in type). So, this code\n * originally defined them as a Proxy wrapper over native dates. The handler\n * intercepted all methods prefixed with `set` and turned them into no-ops.\n *\n * That got really close to working, but then development ran into a critical\n * limitation of the Proxy API. Basically, if the readonly date is defined with\n * a proxy, and you try to call Date.prototype.toISOString.call(readonlyDate),\n * that immediately blows up because the proxy itself is treated as the receiver\n * instead of the underlying native date.\n *\n * Vitest uses .call because it's the more airtight thing to do in most\n * situations, but proxy objects only have traps for .apply calls, not .call. So\n * there is no way in the language to intercept these calls and make sure\n * they're going to the right place. It is a hard, HARD limitation.\n *\n * The good news, though, is that having an extended class seems like the better\n * option, because it gives us the ability to define custom convenience methods\n * without breaking instanceof checks or breaking TypeScript assignability for\n * libraries that expect native dates. We just have to do a little bit of extra\n * work to fudge things for test runners.\n */\n\n/**\n * Any extra methods for readonly dates.\n */\ninterface ReadonlyDateApi {\n\t/**\n\t * Converts a readonly date into a native (mutable) date.\n\t */\n\ttoNativeDate(): Date;\n}\n\n/**\n * A readonly version of a Date object. To maximize compatibility with existing\n * libraries, all methods are the same as the native Date object at the type\n * level. But crucially, all methods prefixed with `set` have all mutation logic\n * removed.\n *\n * If you need a mutable version of the underlying date, ReadonlyDate exposes a\n * .toNativeDate method to do a runtime conversion to a native/mutable date.\n */\nexport class ReadonlyDate extends Date implements ReadonlyDateApi {\n\t// Native dates support such a wide range of arguments (from 0 to 7), so\n\t// conditional types would be incredibly awkward here. Just using\n\t// constructor overloads instead\n\tconstructor();\n\tconstructor(initValue: number | string | Date);\n\tconstructor(year: number, monthIndex: number);\n\tconstructor(year: number, monthIndex: number, day: number);\n\tconstructor(year: number, monthIndex: number, day: number, hours: number);\n\tconstructor(\n\t\tyear: number,\n\t\tmonthIndex: number,\n\t\tday: number,\n\t\thours: number,\n\t\tseconds: number,\n\t);\n\tconstructor(\n\t\tyear: number,\n\t\tmonthIndex: number,\n\t\tday: number,\n\t\thours: number,\n\t\tseconds: number,\n\t\tmilliseconds: number,\n\t);\n\tconstructor(\n\t\tinitValue?: number | string | Date,\n\t\tmonthIndex?: number,\n\t\tday?: number,\n\t\thours?: number,\n\t\tminutes?: number,\n\t\tseconds?: number,\n\t\tmilliseconds?: number,\n\t) {\n\t\t/**\n\t\t * One problem with the native Date type is that they allow you to\n\t\t * produce invalid dates silently, and you won't find out until it's too\n\t\t * late. It's a lot like NaN for numbers.\n\t\t *\n\t\t * Taking some extra steps to make sure that they can't ever creep into\n\t\t * the library and break all the state modeling.\n\t\t *\n\t\t * Strings are still a problem, but that gets taken care of later in the\n\t\t * constructor.\n\t\t */\n\t\tconst hasInvalidSourceDate =\n\t\t\tinitValue instanceof Date && initValue.toString() === \"Invalid Date\";\n\t\tif (hasInvalidSourceDate) {\n\t\t\tthrow new RangeError(\n\t\t\t\t\"Cannot instantiate ReadonlyDate via invalid date object\",\n\t\t\t);\n\t\t}\n\n\t\t/**\n\t\t * biome-ignore lint:complexity/noArguments -- We're going to be using\n\t\t * `arguments` a good bit because the native Date relies on the meta\n\t\t * parameter so much for runtime behavior\n\t\t */\n\t\tconst hasInvalidNums = [...arguments].some((el) => {\n\t\t\t/**\n\t\t\t * You almost never see them in practice, but native dates do\n\t\t\t * support using negative AND fractional values for instantiation.\n\t\t\t * Negative values produce values before 1970.\n\t\t\t */\n\t\t\treturn typeof el === \"number\" && !Number.isFinite(el);\n\t\t});\n\t\tif (hasInvalidNums) {\n\t\t\tthrow new RangeError(\n\t\t\t\t\"Cannot instantiate ReadonlyDate via invalid number(s)\",\n\t\t\t);\n\t\t}\n\n\t\t/**\n\t\t * This guard clause looks incredibly silly, but we need to do this to\n\t\t * make sure that the readonly class works properly with Jest, Vitest,\n\t\t * and anything else that supports fake timers. Critically, it makes\n\t\t * this possible without introducing any extra runtime dependencies.\n\t\t *\n\t\t * Basically:\n\t\t * 1. We need to make sure that ReadonlyDate extends the Date prototype,\n\t\t * so that instanceof checks work correctly, and so that the class\n\t\t * can interop with all libraries that rely on vanilla Dates\n\t\t * 2. In ECMAScript, this linking happens right as the module is\n\t\t * imported\n\t\t * 3. Jest and Vitest will do some degree of hoisting before the\n\t\t * imports get evaluated, but most of the mock functionality happens\n\t\t * at runtime. useFakeTimers is NOT hoisted\n\t\t * 4. A Vitest test file might import the readonly class at some point\n\t\t * (directly or indirectly), which establishes the link\n\t\t * 5. useFakeTimers can then be called after imports, and that updates\n\t\t * the global scope so that when any FUTURE code references the\n\t\t * global Date object, the fake version is used instead\n\t\t * 6. But because the linking already happened before the call,\n\t\t * ReadonlyDate will still be bound to the original Date object\n\t\t * 7. When super is called (which is required when extending classes),\n\t\t * the original date object will be instantiated and then linked to\n\t\t * the readonly instance via the prototype chain\n\t\t * 8. None of this is a problem when you're instantiating the class by\n\t\t * passing it actual inputs, because then the date result will always\n\t\t * be deterministic. The problem happens when you make the date with\n\t\t * no arguments, because that causes a new date to be created with\n\t\t * the true system time, instead of the fake system time.\n\t\t * 9. So, to bridge the gap, we make a separate Date with `new Date()`\n\t\t * (after it's been turned into the fake version), and then use it to\n\t\t * overwrite the contents of the real date created with super\n\t\t */\n\t\tif (initValue === undefined) {\n\t\t\tsuper();\n\t\t\tconst overrideForTestCorrectness = Date.now();\n\t\t\tsuper.setTime(overrideForTestCorrectness);\n\t\t\treturn;\n\t\t}\n\n\t\tif (typeof initValue === \"string\") {\n\t\t\tsuper(initValue);\n\t\t\tif (super.toString() === \"Invalid Date\") {\n\t\t\t\tthrow new RangeError(\n\t\t\t\t\t\"Cannot instantiate ReadonlyDate via invalid string\",\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif (monthIndex === undefined) {\n\t\t\tsuper(initValue);\n\t\t\treturn;\n\t\t}\n\t\tif (typeof initValue !== \"number\") {\n\t\t\tthrow new TypeError(\n\t\t\t\t`Impossible case encountered: init value has type of '${typeof initValue}, but additional arguments were provided after the first`,\n\t\t\t);\n\t\t}\n\n\t\t/**\n\t\t * biome-ignore lint:complexity/noArguments -- Native dates are super\n\t\t * wonky, and they actually check arguments.length to define behavior\n\t\t * at runtime. We can't pass all the arguments in via a single call,\n\t\t * because then the constructor will create an invalid date the moment\n\t\t * it finds any single undefined value.\n\t\t */\n\t\tconst argCount = arguments.length;\n\t\tswitch (argCount) {\n\t\t\tcase 2: {\n\t\t\t\tsuper(initValue, monthIndex);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 3: {\n\t\t\t\tsuper(initValue, monthIndex, day);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 4: {\n\t\t\t\tsuper(initValue, monthIndex, day, hours);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 5: {\n\t\t\t\tsuper(initValue, monthIndex, day, hours, minutes);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 6: {\n\t\t\t\tsuper(initValue, monthIndex, day, hours, minutes, seconds);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 7: {\n\t\t\t\tsuper(\n\t\t\t\t\tinitValue,\n\t\t\t\t\tmonthIndex,\n\t\t\t\t\tday,\n\t\t\t\t\thours,\n\t\t\t\t\tminutes,\n\t\t\t\t\tseconds,\n\t\t\t\t\tmilliseconds,\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tdefault: {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Cannot instantiate new Date with ${argCount} arguments`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\n\ttoNativeDate(): Date {\n\t\tconst time = super.getTime();\n\t\treturn new Date(time);\n\t}\n\n\t////////////////////////////////////////////////////////////////////////////\n\t// Start of custom set methods to shadow the ones from native dates. Note\n\t// that all set methods expect that the underlying timestamp be returned\n\t// afterwards, which always corresponds to Date.getTime.\n\t////////////////////////////////////////////////////////////////////////////\n\n\toverride setDate(_date: number): number {\n\t\treturn super.getTime();\n\t}\n\n\toverride setFullYear(_year: number, _month?: number, _date?: number): number {\n\t\treturn super.getTime();\n\t}\n\n\toverride setHours(\n\t\t_hours: number,\n\t\t_min?: number,\n\t\t_sec?: number,\n\t\t_ms?: number,\n\t): number {\n\t\treturn super.getTime();\n\t}\n\n\toverride setMilliseconds(_ms: number): number {\n\t\treturn super.getTime();\n\t}\n\n\toverride setMinutes(_min: number, _sec?: number, _ms?: number): number {\n\t\treturn super.getTime();\n\t}\n\n\toverride setMonth(_month: number, _date?: number): number {\n\t\treturn super.getTime();\n\t}\n\n\toverride setSeconds(_sec: number, _ms?: number): number {\n\t\treturn super.getTime();\n\t}\n\n\toverride setTime(_time: number): number {\n\t\treturn super.getTime();\n\t}\n\n\toverride setUTCDate(_date: number): number {\n\t\treturn super.getTime();\n\t}\n\n\toverride setUTCFullYear(\n\t\t_year: number,\n\t\t_month?: number,\n\t\t_date?: number,\n\t): number {\n\t\treturn super.getTime();\n\t}\n\n\toverride setUTCHours(\n\t\t_hours: number,\n\t\t_min?: number,\n\t\t_sec?: number,\n\t\t_ms?: number,\n\t): number {\n\t\treturn super.getTime();\n\t}\n\n\toverride setUTCMilliseconds(_ms: number): number {\n\t\treturn super.getTime();\n\t}\n\n\toverride setUTCMinutes(_min: number, _sec?: number, _ms?: number): number {\n\t\treturn super.getTime();\n\t}\n\n\toverride setUTCMonth(_month: number, _date?: number): number {\n\t\treturn super.getTime();\n\t}\n\n\toverride setUTCSeconds(_sec: number, _ms?: number): number {\n\t\treturn super.getTime();\n\t}\n}\n","import { ReadonlyDate } from \"./ReadonlyDate\";\n\n/**\n * A collection of commonly-needed intervals (all defined in milliseconds).\n */\n// Doing type assertion on the static numeric values to prevent compiler from\n// over-inferring the types, and exposing too much info to end users\nexport const refreshRates = Object.freeze({\n\t/**\n\t * Indicates that a subscriber does not strictly need updates, but is still\n\t * allowed to be updated if it would keep it in sync with other subscribers.\n\t *\n\t * If all subscribers use this update interval, TimeSync will never dispatch\n\t * any updates.\n\t */\n\tidle: Number.POSITIVE_INFINITY,\n\n\thalfSecond: 500 as number,\n\toneSecond: 1000 as number,\n\tthirtySeconds: 30_000 as number,\n\toneMinute: 60 * 1000,\n\tfiveMinutes: 5 * 60 * 1000,\n\toneHour: 60 * 60 * 1000,\n}) satisfies Record<string, number>;\n\n/**\n * The set of readonly options that the TimeSync has been configured with.\n */\nexport interface Configuration {\n\t/**\n\t * Indicates whether the TimeSync instance should be frozen for Snapshot\n\t * tests. Highly encouraged that you use this together with\n\t * `initialDate`.\n\t *\n\t * Defaults to false.\n\t */\n\treadonly freezeUpdates: boolean;\n\n\t/**\n\t * The minimum refresh interval (in milliseconds) to use when dispatching\n\t * interval-based state updates.\n\t *\n\t * If a value smaller than this is specified when trying to set up a new\n\t * subscription, this minimum will be used instead.\n\t *\n\t * It is highly recommended that you only modify this value if you have a\n\t * good reason. Updating this value to be too low can make the event loop\n\t * get really hot and really tank performance elsewhere in an application.\n\t *\n\t * Defaults to 200ms.\n\t */\n\treadonly minimumRefreshIntervalMs: number;\n\n\t/**\n\t * Indicates whether the same `onUpdate` callback (by reference) should be\n\t * called multiple time if registered by multiple systems.\n\t *\n\t * If this value is flipped to false, each onUpdate callback will receive\n\t * the subscription context for the FIRST subscriber that registered the\n\t * onUpdate callback.\n\t *\n\t * Defaults to true.\n\t */\n\treadonly allowDuplicateOnUpdateCalls: boolean;\n}\n\n/**\n * The set of options that can be used to instantiate a TimeSync.\n */\nexport interface InitOptions extends Configuration {\n\t/**\n\t * The Date object to use when initializing TimeSync to make the\n\t * constructor more pure and deterministic.\n\t */\n\treadonly initialDate: Date;\n}\n\n/**\n * An object used to initialize a new subscription for TimeSync.\n */\nexport interface SubscriptionInitOptions {\n\t/**\n\t * The maximum update interval that a subscriber needs. A value of\n\t * Number.POSITIVE_INFINITY indicates that the subscriber does not strictly\n\t * need any updates (though they may still happen based on other\n\t * subscribers).\n\t *\n\t * TimeSync always dispatches updates based on the lowest update interval\n\t * among all subscribers.\n\t *\n\t * For example, let's say that we have these three subscribers:\n\t * 1. A - Needs updates no slower than 500ms\n\t * 2. B – Needs updates no slower than 1000ms\n\t * 3. C – Uses interval of Infinity (does not strictly need an update)\n\t *\n\t * A, B, and C will all be updated at a rate of 500ms. If A unsubscribes,\n\t * then B and C will shift to being updated every 1000ms. If B unsubscribes\n\t * after A, updates will pause completely until a new subscriber gets\n\t * added, and it has a non-infinite interval.\n\t */\n\treadonly targetRefreshIntervalMs: number;\n\n\t/**\n\t * The callback to call when a new state update needs to be flushed amongst\n\t * all subscribers.\n\t */\n\treadonly onUpdate: OnTimeSyncUpdate;\n}\n\n/**\n * A complete snapshot of the user-relevant internal state from TimeSync. This\n * value is treated as immutable at both runtime and compile time.\n */\nexport interface Snapshot {\n\t/**\n\t * The date that TimeSync last processed. This will always match the date that\n\t * was last dispatched to all subscribers, but if no updates have been issued,\n\t * this value will match the date used to instantiate the TimeSync.\n\t */\n\treadonly date: ReadonlyDate;\n\n\t/**\n\t * The monotonic milliseconds that elapsed between the TimeSync being\n\t * instantiated and the last update being dispatched.\n\t *\n\t * Will be null if no updates have ever been dispatched.\n\t */\n\treadonly lastUpdatedAtMs: number | null;\n\n\t/**\n\t * The number of subscribers registered with TimeSync.\n\t */\n\treadonly subscriberCount: number;\n\n\t/**\n\t * The configuration options used when instantiating the TimeSync instance.\n\t * The value is guaranteed to be stable for the entire lifetime of TimeSync.\n\t */\n\treadonly config: Configuration;\n}\n\n/**\n * An object with information about a specific subscription registered with\n * TimeSync. The entire context is frozen at runtime.\n */\nexport interface SubscriptionContext {\n\t/**\n\t * A reference to the TimeSync instance that the subscription was registered\n\t * with.\n\t */\n\treadonly timeSync: TimeSync;\n\n\t/**\n\t * The effective interval that the subscription is updating at. This may be a\n\t * value larger than than the target refresh interval, depending on whether\n\t * TimeSync was configured with a minimum refresh value.\n\t */\n\treadonly refreshIntervalMs: number;\n\n\t/**\n\t * The unsubscribe callback associated with a subscription. This is the same\n\t * callback returned by `TimeSync.subscribe`.\n\t */\n\treadonly unsubscribe: () => void;\n\n\t/**\n\t * The monotonic milliseconds that elapsed between the TimeSync being\n\t * instantiated and the subscription being registered.\n\t */\n\treadonly registeredAtMs: number;\n}\n\n/**\n * The callback to call when a new state update is ready to be dispatched.\n */\nexport type OnTimeSyncUpdate = (\n\tnewDate: ReadonlyDate,\n\tcontext: SubscriptionContext,\n) => void;\n\ninterface TimeSyncApi {\n\t/**\n\t * Subscribes an external system to TimeSync.\n\t *\n\t * The same callback (by reference) is allowed to be registered multiple\n\t * times, either for the same update interval, or different update\n\t * intervals. Depending on how TimeSync is instantiated, it may choose to\n\t * de-duplicate these function calls on each round of updates.\n\t *\n\t * If a value of Number.POSITIVE_INFINITY is used, the subscription will be\n\t * considered \"idle\". Idle subscriptions cannot trigger updates on their\n\t * own, but can stay in the loop as otherupdates get dispatched from via\n\t * other subscriptions.\n\t *\n\t * Consider using the refreshRates object from this package for a set of\n\t * commonly-used intervals.\n\t *\n\t * @throws {RangeError} If the provided interval is neither a positive\n\t * integer nor positive infinity.\n\t * @returns An unsubscribe callback. Calling the callback more than once\n\t * results in a no-op.\n\t */\n\tsubscribe: (options: SubscriptionInitOptions) => () => void;\n\n\t/**\n\t * Allows an external system to pull an immutable snapshot of some of the\n\t * internal state inside TimeSync. The snapshot is frozen at runtime and\n\t * cannot be mutated.\n\t *\n\t * @returns An object with multiple properties describing the TimeSync.\n\t */\n\tgetStateSnapshot: () => Snapshot;\n\n\t/**\n\t * Resets all internal state in the TimeSync, and handles all cleanup for\n\t * subscriptions and intervals previously set up. Configuration values are\n\t * retained.\n\t *\n\t * This method can be used as a dispose method for a locally-scoped\n\t * TimeSync (a TimeSync with no subscribers is safe to garbage-collect\n\t * without any risks of memory leaks). It can also be used to reset a global\n\t * TimeSync to its initial state for certain testing setups.\n\t */\n\tclearAll: () => void;\n}\n\n/**\n * Even though both the browser and the server are able to give monotonic times\n * that are at least as precise as a nanosecond, we're using milliseconds for\n * consistency with useInterval, which cannot be more precise than a\n * millisecond.\n */\nfunction getMonotonicTimeMs(): number {\n\t// If we're on the server, we can use process.hrtime, which is defined for\n\t// Node, Deno, and Bun\n\tif (typeof window === \"undefined\") {\n\t\tconst timeInNanoseconds = process.hrtime.bigint();\n\t\treturn Number(timeInNanoseconds / 1000n);\n\t}\n\n\t// Otherwise, we need to get the high-resolution timestamp from the browser.\n\t// This value is fractional and goes to nine decimal places\n\tconst highResTimestamp = window.performance.now();\n\treturn Math.floor(highResTimestamp);\n}\n\n/**\n * This function is just a convenience for us to sidestep some problems around\n * TypeScript's LSP and Object.freeze. Because Object.freeze can accept any\n * arbitrary type, it basically acts as a \"type boundary\" between the left and\n * right sides of any snapshot assignments.\n *\n * That means that if you rename a property a a value that is passed to\n * Object.freeze, the LSP can't auto-rename it, and you potentially get missing\n * properties. This is a bit hokey, but because the function is defined strictly\n * in terms of concrete snapshots, any value passed to this function won't have\n * to worry about mismatches.\n */\nfunction freezeSnapshot(snap: Snapshot): Snapshot {\n\tif (!Object.isFrozen(snap.config)) {\n\t\tObject.freeze(snap.config);\n\t}\n\tif (!Object.isFrozen(snap)) {\n\t\tObject.freeze(snap);\n\t}\n\treturn snap;\n}\n\nconst defaultMinimumRefreshIntervalMs = 200;\n\n/**\n * One thing that was considered was giving TimeSync the ability to flip which\n * kinds of dates it uses, and let it use native dates instead of readonly\n * dates. We type readonly dates as native dates for better interoperability\n * with pretty much every JavaScript library under the sun, but there is still a\n * big difference in runtime behavior. There is a risk that blocking mutations\n * could break some other library in other ways.\n *\n * That might be worth revisiting if we get user feedback, but right now, it\n * seems like an incredibly bad idea.\n *\n * 1. Any single mutation has a risk of breaking the entire integrity of the\n * system. If a consumer would try to mutate them, things SHOULD blow up by\n * default.\n * 2. Dates are a type of object that are far more read-heavy than write-heavy,\n * so the risks of breaking are generally lower\n * 3. If a user really needs a mutable version of the date, they can make a\n * mutable copy first via `const mutable = readonlyDate.toNativeDate()`\n *\n * The one case when turning off the readonly behavior would be good would be\n * if you're on a server that really needs to watch its garbage collection\n * output, and you the overhead from the readonly date is causing too much\n * pressure on resources. In that case, you could switch to native dates, but\n * you'd still need a LOT of trigger discipline to avoid mutations, especially\n * if you rely on outside libraries.\n */\n/**\n * TimeSync provides a centralized authority for working with time values in a\n * more structured way. It ensures all dependents for the time values stay in\n * sync with each other.\n *\n * (e.g., In a React codebase, you want multiple components that rely on time\n * values to update together, to avoid screen tearing and stale data for only\n * some parts of the screen.)\n */\nexport class TimeSync implements TimeSyncApi {\n\t/**\n\t * The monotonic time in milliseconds from when the TimeSync instance was\n\t * first instantiated.\n\t */\n\treadonly #initializedAtMs: number;\n\n\t/**\n\t * Stores all refresh intervals actively associated with an onUpdate\n\t * callback (along with their associated unsubscribe callbacks).\n\t *\n\t * Supports storing the exact same callback-interval pairs multiple times,\n\t * in case multiple external systems need to subscribe with the exact same\n\t * data concerns. Because the functions themselves are used as keys, that\n\t * ensures that each callback will only be called once per update, no matter\n\t * how subscribers use it.\n\t *\n\t * Each map value should stay sorted by refresh interval, in ascending\n\t * order.\n\t *\n\t * ---\n\t *\n\t * This is a rare case where we actually REALLY need the readonly modifier\n\t * to avoid infinite loops. JavaScript's iterator protocol is really great\n\t * for making loops simple and type-safe, but because subscriptions have the\n\t * ability to add more subscriptions, we need to make an immutable version\n\t * of each array at some point to make sure that we're not iterating through\n\t * values forever\n\t *\n\t * We can choose to do that at one of two points:\n\t * 1. When adding a new subscription\n\t * 2. When dispatching a new round of updates\n\t *\n\t * Because this library assumes that dispatches will be much more common\n\t * than new subscriptions (a single subscription that subscribes for one\n\t * second will receive 360 updates in five minutes), operations should be\n\t * done to optimize that use case. So we should move the immutability costs\n\t * to the subscribe and unsubscribe operations.\n\t */\n\t#subscriptions: Map<OnTimeSyncUpdate, readonly SubscriptionContext[]>;\n\n\t/**\n\t * The latest public snapshot of TimeSync's internal state. The snapshot\n\t * should always be treated as an immutable value.\n\t */\n\t#latestSnapshot: Snapshot;\n\n\t/**\n\t * A cached version of the fastest interval currently registered with\n\t * TimeSync. Should always be derived from #subscriptions\n\t */\n\t#fastestRefreshInterval: number;\n\n\t/**\n\t * Used for both its intended purpose (creating interval), but also as a\n\t * janky version of setTimeout. Also, all versions of setInterval are\n\t * monotonic, so we don't have to do anything special for it.\n\t *\n\t * There are a few times when we need timeout-like logic, but if we use\n\t * setInterval for everything, we have fewer IDs to juggle, and less risk of\n\t * things getting out of sync.\n\t *\n\t * Type defined like this to support client and server behavior. Node.js\n\t * uses its own custom timeout type, but Deno, Bun, and the browser all use\n\t * the number type.\n\t */\n\t#intervalId: NodeJS.Timeout | number | undefined;\n\n\tconstructor(options?: Partial<InitOptions>) {\n\t\tconst {\n\t\t\tinitialDate,\n\t\t\tfreezeUpdates = false,\n\t\t\tallowDuplicateOnUpdateCalls = true,\n\t\t\tminimumRefreshIntervalMs = defaultMinimumRefreshIntervalMs,\n\t\t} = options ?? {};\n\n\t\tconst isMinValid =\n\t\t\tNumber.isInteger(minimumRefreshIntervalMs) &&\n\t\t\tminimumRefreshIntervalMs > 0;\n\t\tif (!isMinValid) {\n\t\t\tthrow new RangeError(\n\t\t\t\t`Minimum refresh interval must be a positive integer (received ${minimumRefreshIntervalMs} ms)`,\n\t\t\t);\n\t\t}\n\n\t\tthis.#subscriptions = new Map();\n\t\tthis.#fastestRefreshInterval = Number.POSITIVE_INFINITY;\n\t\tthis.#intervalId = undefined;\n\t\tthis.#initializedAtMs = getMonotonicTimeMs();\n\n\t\tlet date: ReadonlyDate;\n\t\tif (initialDate instanceof ReadonlyDate) {\n\t\t\tdate = initialDate;\n\t\t} else if (initialDate instanceof Date) {\n\t\t\tdate = new ReadonlyDate(initialDate);\n\t\t} else {\n\t\t\tdate = new ReadonlyDate();\n\t\t}\n\n\t\tthis.#latestSnapshot = freezeSnapshot({\n\t\t\tdate,\n\t\t\tsubscriberCount: 0,\n\t\t\tlastUpdatedAtMs: null,\n\t\t\tconfig: {\n\t\t\t\tfreezeUpdates,\n\t\t\t\tminimumRefreshIntervalMs,\n\t\t\t\tallowDuplicateOnUpdateCalls,\n\t\t\t},\n\t\t});\n\t}\n\n\t#notifyAllSubscriptions(): void {\n\t\t// It's more important that we copy the date object into a separate\n\t\t// variable here than normal, because need make sure the `this` context\n\t\t// can't magically change between updates and cause subscribers to\n\t\t// receive different values\n\t\tconst { date, config } = this.#latestSnapshot;\n\n\t\tconst subscriptionsPaused =\n\t\t\tconfig.freezeUpdates ||\n\t\t\tthis.#subscriptions.size === 0 ||\n\t\t\tthis.#fastestRefreshInterval === Number.POSITIVE_INFINITY;\n\t\tif (subscriptionsPaused) {\n\t\t\treturn;\n\t\t}\n\n\t\t/**\n\t\t * Two things:\n\t\t * 1. Even though the context arrays are defined as readonly (which\n\t\t * removes on the worst edge cases during dispatching), the\n\t\t * subscriptions map itself is still mutable, so there are a few edge\n\t\t * cases we need to deal with. While the risk of infinite loops should\n\t\t * be much lower, there's still the risk that an onUpdate callback could\n\t\t * add a subscriber for an interval that wasn't registered before, which\n\t\t * the iterator protocol will pick up. Need to make a local,\n\t\t * fixed-length copy of the map entries before starting iteration. Any\n\t\t * subscriptions added during update will just have to wait until the\n\t\t * next round of updates.\n\t\t *\n\t\t * 2. The trade off of the serialization is that we do lose the ability\n\t\t * to auto-break the loop if one of the subscribers ends up resetting\n\t\t * all state, because we'll still have local copies of entries. We need\n\t\t * to check on each iteration to see if we should continue.\n\t\t */\n\t\tconst subsBeforeUpdate = this.#subscriptions;\n\t\tconst localEntries = Array.from(subsBeforeUpdate);\n\t\touter: for (const [onUpdate, subs] of localEntries) {\n\t\t\tfor (const ctx of subs) {\n\t\t\t\t// We're not doing anything more sophisticated here because\n\t\t\t\t// we're assuming that any systems that can clear out the\n\t\t\t\t// subscriptions will handle cleaning up each context, too\n\t\t\t\tconst wasClearedBetweenUpdates = subsBeforeUpdate.size === 0;\n\t\t\t\tif (wasClearedBetweenUpdates) {\n\t\t\t\t\tbreak outer;\n\t\t\t\t}\n\n\t\t\t\tonUpdate(date, ctx);\n\t\t\t\tif (!config.allowDuplicateOnUpdateCalls) {\n\t\t\t\t\tcontinue outer;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * The logic that should happen at each step in TimeSync's active interval.\n\t *\n\t * Defined as an arrow function so that we can just pass it directly to\n\t * setInterval without needing to make a new wrapper function each time. We\n\t * don't have many situations where we can lose the `this` context, but this\n\t * is one of them.\n\t */\n\treadonly #onTick = (): void => {\n\t\tconst { config } = this.#latestSnapshot;\n\t\tif (config.freezeUpdates) {\n\t\t\t// Defensive step to make sure that an invalid tick wasn't started\n\t\t\tclearInterval(this.#intervalId);\n\t\t\tthis.#intervalId = undefined;\n\t\t\treturn;\n\t\t}\n\n\t\t// onTick is expected to be called in response to monotonic time changes\n\t\t// (either from calculating them manually to decide when to call onTick\n\t\t// synchronously or from letting setInterval handle the calls). So while\n\t\t// this edge case should basically be impossible, we need to make sure that\n\t\t// we always dispatch a date, even if its time is exactly the same.\n\t\tthis.#latestSnapshot = freezeSnapshot({\n\t\t\t...this.#latestSnapshot,\n\t\t\tdate: new ReadonlyDate(),\n\t\t\tlastUpdatedAtMs: getMonotonicTimeMs() - this.#initializedAtMs,\n\t\t});\n\t\tthis.#notifyAllSubscriptions();\n\t};\n\n\t#onFastestIntervalChange(): void {\n\t\tconst fastest = this.#fastestRefreshInterval;\n\t\tconst { lastUpdatedAtMs, config } = this.#latestSnapshot;\n\n\t\tconst updatesShouldStop =\n\t\t\tconfig.freezeUpdates ||\n\t\t\tthis.#subscriptions.size === 0 ||\n\t\t\tfastest === Number.POSITIVE_INFINITY;\n\t\tif (updatesShouldStop) {\n\t\t\tclearInterval(this.#intervalId);\n\t\t\tthis.#intervalId = undefined;\n\t\t\treturn;\n\t\t}\n\n\t\tconst newTime = getMonotonicTimeMs();\n\t\tconst elapsed = newTime - (lastUpdatedAtMs ?? this.#initializedAtMs);\n\t\tconst timeBeforeNextUpdate = fastest - elapsed;\n\n\t\t// Clear previous interval no matter what just to be on the safe side\n\t\tclearInterval(this.#intervalId);\n\n\t\tif (timeBeforeNextUpdate <= 0) {\n\t\t\tthis.#onTick();\n\t\t\tthis.#intervalId = setInterval(this.#onTick, fastest);\n\t\t\treturn;\n\t\t}\n\n\t\t// Most common case for this branch is the very first subscription\n\t\t// getting added, but there's still the small chance that the fastest\n\t\t// interval could change right after an update got flushed, so there would\n\t\t// be zero elapsed time to worry about\n\t\tif (timeBeforeNextUpdate === fastest) {\n\t\t\tthis.#intervalId = setInterval(this.#onTick, timeBeforeNextUpdate);\n\t\t\treturn;\n\t\t}\n\n\t\t// Otherwise, use setInterval as pseudo-timeout to resolve the remaining\n\t\t// time as a one-time update, and then go back to using normal intervals\n\t\tthis.#intervalId = setInterval(() => {\n\t\t\tclearInterval(this.#intervalId);\n\n\t\t\t// Need to set up interval before ticking in the tiny, tiny chance\n\t\t\t// that ticking would cause the TimeSync instance to be reset. We\n\t\t\t// don't want to start a new interval right after we've lost our\n\t\t\t// ability to do cleanup. The timer won't start getting processed\n\t\t\t// until the function leaves scope anyway\n\t\t\tthis.#intervalId = setInterval(this.#onTick, fastest);\n\t\t\tthis.#onTick();\n\t\t}, timeBeforeNextUpdate);\n\t}\n\n\t#updateFastestInterval(): void {\n\t\tconst { config } = this.#latestSnapshot;\n\t\tif (config.freezeUpdates) {\n\t\t\tthis.#fastestRefreshInterval = Number.POSITIVE_INFINITY;\n\t\t\treturn;\n\t\t}\n\n\t\t// This setup requires that every interval array stay sorted. It\n\t\t// immediately falls apart if this isn't guaranteed.\n\t\tconst prevFastest = this.#fastestRefreshInterval;\n\t\tlet newFastest = Number.POSITIVE_INFINITY;\n\t\tfor (const contexts of this.#subscriptions.values()) {\n\t\t\tconst subFastest =\n\t\t\t\tcontexts[0]?.refreshIntervalMs ?? Number.POSITIVE_INFINITY;\n\t\t\tif (subFastest < newFastest) {\n\t\t\t\tnewFastest = subFastest;\n\t\t\t}\n\t\t}\n\n\t\tthis.#fastestRefreshInterval = newFastest;\n\t\tif (prevFastest !== newFastest) {\n\t\t\tthis.#onFastestIntervalChange();\n\t\t}\n\t}\n\n\tsubscribe(options: SubscriptionInitOptions): () => void {\n\t\t// Destructuring properties so that they can't be fiddled with after\n\t\t// this function call ends\n\t\tconst { targetRefreshIntervalMs, onUpdate } = options;\n\t\tconst { minimumRefreshIntervalMs } = this.#latestSnapshot.config;\n\n\t\tconst isTargetValid =\n\t\t\ttargetRefreshIntervalMs === Number.POSITIVE_INFINITY ||\n\t\t\t(Number.isInteger(targetRefreshIntervalMs) &&\n\t\t\t\ttargetRefreshIntervalMs > 0);\n\t\tif (!isTargetValid) {\n\t\t\tthrow new Error(\n\t\t\t\t`Target refresh interval must be positive infinity or a positive integer (received ${targetRefreshIntervalMs} ms)`,\n\t\t\t);\n\t\t}\n\n\t\tconst subsOnSetup = this.#subscriptions;\n\t\tlet subscribed = true;\n\t\tconst ctx: SubscriptionContext = {\n\t\t\ttimeSync: this,\n\t\t\tregisteredAtMs: getMonotonicTimeMs() - this.#initializedAtMs,\n\t\t\trefreshIntervalMs: Math.max(\n\t\t\t\tminimumRefreshIntervalMs,\n\t\t\t\ttargetRefreshIntervalMs,\n\t\t\t),\n\n\t\t\tunsubscribe: () => {\n\t\t\t\t// Not super conventional, but basically using try/finally as a form of\n\t\t\t\t// Go's defer. There are a lot of branches we need to worry about for\n\t\t\t\t// the unsubscribe callback, and we need to make sure we flip subscribed\n\t\t\t\t// to false after each one\n\t\t\t\ttry {\n\t\t\t\t\tif (!subscribed || this.#subscriptions !== subsOnSetup) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconst contexts = subsOnSetup.get(onUpdate);\n\t\t\t\t\tif (contexts === undefined) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconst filtered = contexts.filter(\n\t\t\t\t\t\t(c) => c.unsubscribe !== ctx.unsubscribe,\n\t\t\t\t\t);\n\t\t\t\t\tif (filtered.length === contexts.length) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst dropped = Math.max(0, this.#latestSnapshot.subscriberCount - 1);\n\t\t\t\t\tthis.#latestSnapshot = freezeSnapshot({\n\t\t\t\t\t\t...this.#latestSnapshot,\n\t\t\t\t\t\tsubscriberCount: dropped,\n\t\t\t\t\t});\n\n\t\t\t\t\tif (filtered.length > 0) {\n\t\t\t\t\t\t// No need to sort on removal because everything gets sorted as\n\t\t\t\t\t\t// it enters the subscriptions map\n\t\t\t\t\t\tsubsOnSetup.set(onUpdate, filtered);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsubsOnSetup.delete(onUpdate);\n\t\t\t\t\t}\n\n\t\t\t\t\tthis.#updateFastestInterval();\n\t\t\t\t} finally {\n\t\t\t\t\tsubscribed = false;\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t\tObject.freeze(ctx);\n\n\t\t// We need to make sure that each array for tracking subscriptions is\n\t\t// readonly, and because dispatching updates should be far more common than\n\t\t// adding subscriptions, we're placing the immutable copying here to\n\t\t// minimize overall pressure on the system.\n\t\tlet newContexts: SubscriptionContext[];\n\t\tconst prevContexts = subsOnSetup.get(onUpdate);\n\t\tif (prevContexts !== undefined) {\n\t\t\tnewContexts = [...prevContexts, ctx];\n\t\t} else {\n\t\t\tnewContexts = [ctx];\n\t\t}\n\n\t\tsubsOnSetup.set(onUpdate, newContexts);\n\t\tnewContexts.sort((c1, c2) => c1.refreshIntervalMs - c2.refreshIntervalMs);\n\n\t\tthis.#latestSnapshot = freezeSnapshot({\n\t\t\t...this.#latestSnapshot,\n\t\t\tsubscriberCount: this.#latestSnapshot.subscriberCount + 1,\n\t\t});\n\n\t\tthis.#updateFastestInterval();\n\t\treturn ctx.unsubscribe;\n\t}\n\n\tgetStateSnapshot(): Snapshot {\n\t\treturn this.#latestSnapshot;\n\t}\n\n\tclearAll(): void {\n\t\tclearInterval(this.#intervalId);\n\t\tthis.#intervalId = undefined;\n\t\tthis.#fastestRefreshInterval = Number.POSITIVE_INFINITY;\n\n\t\t// As long as we clean things the internal state, it's safe not to\n\t\t// bother calling each unsubscribe callback. Not calling them one by\n\t\t// one actually has much better time complexity\n\t\tthis.#subscriptions.clear();\n\n\t\t// We swap the map out so that the unsubscribe callbacks can detect\n\t\t// whether their functionality is still relevant\n\t\tthis.#subscriptions = new Map();\n\n\t\tthis.#latestSnapshot = freezeSnapshot({\n\t\t\t...this.#latestSnapshot,\n\t\t\tsubscriberCount: 0,\n\t\t});\n\t}\n}\n"],"mappings":"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"}
|