@buenos-nachos/time-sync 0.5.5 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts 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
@@ -312,5 +260,4 @@ declare class TimeSync implements TimeSyncApi {
312
260
  clearAll(): void;
313
261
  }
314
262
  //#endregion
315
- export { type Configuration, type InitOptions, type OnTimeSyncUpdate, ReadonlyDate, type Snapshot, type SubscriptionContext, type SubscriptionInitOptions, TimeSync, refreshRates };
316
- //# sourceMappingURL=index.d.ts.map
263
+ export { type Configuration, type InitOptions, type OnTimeSyncUpdate, ReadonlyDate, type Snapshot, type SubscriptionContext, type SubscriptionInitOptions, TimeSync, refreshRates };
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) {
@@ -468,5 +295,4 @@ var TimeSync = class {
468
295
  };
469
296
 
470
297
  //#endregion
471
- export { ReadonlyDate, TimeSync, refreshRates };
472
- //# sourceMappingURL=index.js.map
298
+ export { ReadonlyDate, TimeSync, refreshRates };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buenos-nachos/time-sync",
3
- "version": "0.5.5",
3
+ "version": "0.6.1",
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
- "./src",
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.mjs",
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