@buenos-nachos/time-sync 0.5.5 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- 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/dist/index.d.ts
CHANGED
|
@@ -312,5 +312,4 @@ declare class TimeSync implements TimeSyncApi {
|
|
|
312
312
|
clearAll(): void;
|
|
313
313
|
}
|
|
314
314
|
//#endregion
|
|
315
|
-
export { type Configuration, type InitOptions, type OnTimeSyncUpdate, ReadonlyDate, type Snapshot, type SubscriptionContext, type SubscriptionInitOptions, TimeSync, refreshRates };
|
|
316
|
-
//# sourceMappingURL=index.d.ts.map
|
|
315
|
+
export { type Configuration, type InitOptions, type OnTimeSyncUpdate, ReadonlyDate, type Snapshot, type SubscriptionContext, type SubscriptionInitOptions, TimeSync, refreshRates };
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buenos-nachos/time-sync",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Michael Smith <hello@nachos.dev> (https://www.nachos.dev)",
|
|
@@ -16,10 +16,7 @@
|
|
|
16
16
|
"ui"
|
|
17
17
|
],
|
|
18
18
|
"files": [
|
|
19
|
-
"./
|
|
20
|
-
"./dist",
|
|
21
|
-
"CHANGELOG.md",
|
|
22
|
-
"!*.test.ts"
|
|
19
|
+
"./dist"
|
|
23
20
|
],
|
|
24
21
|
"sideEffects": false,
|
|
25
22
|
"main": "./dist/index.js",
|
|
@@ -29,6 +26,6 @@
|
|
|
29
26
|
"scripts": {
|
|
30
27
|
"check:types": "tsc --noEmit"
|
|
31
28
|
},
|
|
32
|
-
"module": "./dist/index.
|
|
29
|
+
"module": "./dist/index.js",
|
|
33
30
|
"types": "./dist/index.d.ts"
|
|
34
31
|
}
|
package/CHANGELOG.md
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
# @buenos-nachos/time-sync
|
|
2
|
-
|
|
3
|
-
## 0.5.5
|
|
4
|
-
|
|
5
|
-
### Patch Changes
|
|
6
|
-
|
|
7
|
-
- 1862a8b: Added explicit build process when preparing NPM scripts to ensure ./dist files can't be omitted
|
|
8
|
-
|
|
9
|
-
## 0.5.4
|
|
10
|
-
|
|
11
|
-
### Patch Changes
|
|
12
|
-
|
|
13
|
-
- 3f130f1: updated `files` in package.json to include accidentally removed files
|
|
14
|
-
|
|
15
|
-
## 0.5.3
|
|
16
|
-
|
|
17
|
-
### Patch Changes
|
|
18
|
-
|
|
19
|
-
- e401ae4: further updated which files are included in NPM packages
|
|
20
|
-
|
|
21
|
-
## 0.5.2
|
|
22
|
-
|
|
23
|
-
### Patch Changes
|
|
24
|
-
|
|
25
|
-
- a2a6843: Removed test files from NPM builds.
|
|
26
|
-
|
|
27
|
-
## 0.5.1
|
|
28
|
-
|
|
29
|
-
### Patch Changes
|
|
30
|
-
|
|
31
|
-
- 5fdc201: Updated wording on `Snapshot.date` to be less misleading.
|
|
32
|
-
|
|
33
|
-
## 0.5.0
|
|
34
|
-
|
|
35
|
-
### Breaking Changes
|
|
36
|
-
|
|
37
|
-
- c3986e9: revamped all state management and APIs to be based on monotonic time
|
|
38
|
-
- c3986e9: Removed `registeredAt` and `intervalLastFulfilledAt` properties from `SubscriptionContext` and added monotonic `registeredAtMs`
|
|
39
|
-
- c3986e9: Added monotonic `lastUpdatedAt` property to `Snapshot` type.
|
|
40
|
-
|
|
41
|
-
## 0.4.1
|
|
42
|
-
|
|
43
|
-
### Patch Changes
|
|
44
|
-
|
|
45
|
-
- 5f37f1a: refactored class to remove private setSnapshost method
|
|
46
|
-
|
|
47
|
-
## 0.4.0
|
|
48
|
-
|
|
49
|
-
### Breaking Changes
|
|
50
|
-
|
|
51
|
-
- 663479e: Removed `isSubscribed` property from context and made all other context properties readonly.
|
|
52
|
-
|
|
53
|
-
## 0.3.2
|
|
54
|
-
|
|
55
|
-
### Patch Changes
|
|
56
|
-
|
|
57
|
-
- b8fbaf8: cleanup up comments and types for exported class, methods, and types.
|
|
58
|
-
|
|
59
|
-
## 0.3.1
|
|
60
|
-
|
|
61
|
-
### Patch Changes
|
|
62
|
-
|
|
63
|
-
- 5fce018: switched internal implementations to use Date.now more often to reduce memory usage
|
|
64
|
-
|
|
65
|
-
## 0.3.0
|
|
66
|
-
|
|
67
|
-
### Breaking Changes
|
|
68
|
-
|
|
69
|
-
- 122f6c1: Updated `SubscriptionContext.timeSync` type to be readonly and non-nullable, and renamed `SubscriptionContext.isLive` to `SubscriptionContext.isSubscribed`.
|
|
70
|
-
|
|
71
|
-
## 0.2.0
|
|
72
|
-
|
|
73
|
-
### Breaking Changes
|
|
74
|
-
|
|
75
|
-
- 2f527dd: Changed the default value of `allowDuplicateFunctionCalls` from `false` to `true`
|
|
76
|
-
|
|
77
|
-
### Minor Changes
|
|
78
|
-
|
|
79
|
-
- 5f86fac: Added second parameter to `onUpdate` callback. This value is a value of type `SubscriptionContext` and provides information about the current subscription.
|
|
80
|
-
|
|
81
|
-
## 0.1.2
|
|
82
|
-
|
|
83
|
-
### Patch Changes
|
|
84
|
-
|
|
85
|
-
- 6189eb2: add README to root directory
|
|
86
|
-
|
|
87
|
-
## 0.1.1
|
|
88
|
-
|
|
89
|
-
### Patch Changes
|
|
90
|
-
|
|
91
|
-
- f18d71c: fix: specified module type as ESM
|
|
92
|
-
|
|
93
|
-
## 0.1.0
|
|
94
|
-
|
|
95
|
-
### Minor Changes
|
|
96
|
-
|
|
97
|
-
- 8be4b26: Initial release
|
package/dist/index.cjs
DELETED
|
@@ -1,475 +0,0 @@
|
|
|
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
|