@buenos-nachos/time-sync 0.3.1 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @buenos-nachos/time-sync
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Breaking Changes
6
+
7
+ - 663479e: Removed `isSubscribed` property from context and made all other context properties readonly.
8
+
9
+ ## 0.3.2
10
+
11
+ ### Patch Changes
12
+
13
+ - b8fbaf8: cleanup up comments and types for exported class, methods, and types.
14
+
3
15
  ## 0.3.1
4
16
 
5
17
  ### Patch Changes
@@ -8,7 +20,7 @@
8
20
 
9
21
  ## 0.3.0
10
22
 
11
- ### Minor Changes
23
+ ### Breaking Changes
12
24
 
13
25
  - 122f6c1: Updated `SubscriptionContext.timeSync` type to be readonly and non-nullable, and renamed `SubscriptionContext.isLive` to `SubscriptionContext.isSubscribed`.
14
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buenos-nachos/time-sync",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "author": "Michael Smith <hello@nachos.dev> (https://www.nachos.dev)",
@@ -131,7 +131,6 @@ describe(TimeSync, () => {
131
131
 
132
132
  const expectedCtx: SubscriptionContext = {
133
133
  unsubscribe,
134
- isSubscribed: true,
135
134
  timeSync: sync,
136
135
  intervalLastFulfilledAt: dateAfter,
137
136
  registeredAt: dateBefore,
@@ -563,28 +562,6 @@ describe(TimeSync, () => {
563
562
  const dateAfter = sync.getStateSnapshot().date;
564
563
  expect(dateAfter).not.toEqual(dateBefore);
565
564
  });
566
-
567
- it("Mutates the isLive context value to be false on unsubscribe", async ({
568
- expect,
569
- }) => {
570
- const sync = new TimeSync();
571
-
572
- let ejectedContext: SubscriptionContext | undefined;
573
- const onUpdate = vi.fn((_: unknown, ctx: SubscriptionContext) => {
574
- ejectedContext = ctx;
575
- });
576
-
577
- const unsub = sync.subscribe({
578
- onUpdate,
579
- targetRefreshIntervalMs: refreshRates.oneMinute,
580
- });
581
-
582
- await vi.advanceTimersByTimeAsync(refreshRates.oneMinute);
583
- expect(ejectedContext?.isSubscribed).toBe(true);
584
-
585
- unsub();
586
- expect(ejectedContext?.isSubscribed).toBe(false);
587
- });
588
565
  });
589
566
 
590
567
  describe("Subscriptions: context values", () => {
@@ -827,7 +804,6 @@ describe(TimeSync, () => {
827
804
 
828
805
  await vi.advanceTimersByTimeAsync(refreshRates.oneSecond);
829
806
  expect(ejectedContext).toEqual<SubscriptionContext>({
830
- isSubscribed: true,
831
807
  intervalLastFulfilledAt: null,
832
808
  registeredAt: snapBefore,
833
809
  targetRefreshIntervalMs: refreshRates.oneHour,
@@ -840,7 +816,6 @@ describe(TimeSync, () => {
840
816
 
841
817
  const snapAfter = sync.getStateSnapshot().date;
842
818
  expect(ejectedContext).toEqual<SubscriptionContext>({
843
- isSubscribed: true,
844
819
  intervalLastFulfilledAt: snapAfter,
845
820
  registeredAt: snapBefore,
846
821
  targetRefreshIntervalMs: refreshRates.oneHour,
@@ -1086,28 +1061,6 @@ describe(TimeSync, () => {
1086
1061
  await vi.advanceTimersByTimeAsync(refreshRates.oneMinute);
1087
1062
  expect(sharedOnUpdate).not.toHaveBeenCalled();
1088
1063
  });
1089
-
1090
- it("Mutates the isLive context value to be false", async ({ expect }) => {
1091
- const sync = new TimeSync();
1092
-
1093
- let ejectedContext: SubscriptionContext | undefined;
1094
- const onUpdate = vi.fn((_: unknown, ctx: SubscriptionContext) => {
1095
- if (ejectedContext === undefined) {
1096
- ejectedContext = ctx;
1097
- }
1098
- });
1099
-
1100
- void sync.subscribe({
1101
- onUpdate,
1102
- targetRefreshIntervalMs: refreshRates.oneMinute,
1103
- });
1104
-
1105
- await vi.advanceTimersByTimeAsync(refreshRates.oneMinute);
1106
- expect(ejectedContext?.isSubscribed).toBe(true);
1107
-
1108
- sync.clearAll();
1109
- expect(ejectedContext?.isSubscribed).toBe(false);
1110
- });
1111
1064
  });
1112
1065
 
1113
1066
  /**
package/src/TimeSync.ts CHANGED
@@ -30,7 +30,8 @@ export const refreshRates = Object.freeze({
30
30
  export interface Configuration {
31
31
  /**
32
32
  * Indicates whether the TimeSync instance should be frozen for Snapshot
33
- * tests.
33
+ * tests. Highly encouraged that you use this together with
34
+ * `initialDate`.
34
35
  *
35
36
  * Defaults to false.
36
37
  */
@@ -44,7 +45,7 @@ export interface Configuration {
44
45
  * subscription, this minimum will be used instead.
45
46
  *
46
47
  * It is highly recommended that you only modify this value if you have a
47
- * good reason. Updating this value to be too low and make the event loop
48
+ * good reason. Updating this value to be too low can make the event loop
48
49
  * get really hot and really tank performance elsewhere in an application.
49
50
  *
50
51
  * Defaults to 200ms.
@@ -55,9 +56,11 @@ export interface Configuration {
55
56
  * Indicates whether the same `onUpdate` callback (by reference) should be
56
57
  * called multiple time if registered by multiple systems.
57
58
  *
58
- * Defaults to true. If this value is flipped to false, each onUpdate
59
- * callback will receive the subscription context for the FIRST subscriber
60
- * that registered the onUpdate callback.
59
+ * If this value is flipped to false, each onUpdate callback will receive
60
+ * the subscription context for the FIRST subscriber that registered the
61
+ * onUpdate callback.
62
+ *
63
+ * Defaults to true.
61
64
  */
62
65
  readonly allowDuplicateOnUpdateCalls: boolean;
63
66
  }
@@ -66,16 +69,6 @@ export interface Configuration {
66
69
  * The set of options that can be used to instantiate a TimeSync.
67
70
  */
68
71
  export interface InitOptions extends Configuration {
69
- /**
70
- * Indicates whether the TimeSync instance should be frozen for snapshot
71
- * tests. Highly encouraged that you use this together with
72
- * `initialDate`.
73
- *
74
- * Defaults to false.
75
- */
76
- // Duplicated property to override the LSP comment
77
- readonly freezeUpdates: boolean;
78
-
79
72
  /**
80
73
  * The Date object to use when initializing TimeSync to make the
81
74
  * constructor more pure and deterministic.
@@ -142,9 +135,8 @@ export interface Snapshot {
142
135
  * TimeSync.
143
136
  *
144
137
  * For performance reasons, this object has ZERO readonly guarantees enforced at
145
- * runtime. A few properties are flagged as readonly at the type level, but
146
- * misuse of this value has a risk of breaking a TimeSync instance's internal
147
- * state. Proceed with caution.
138
+ * runtime. All properties are defined as readonly at the type level, but an
139
+ * accidental mutation can still slip through.
148
140
  */
149
141
  export interface SubscriptionContext {
150
142
  /**
@@ -169,12 +161,6 @@ export interface SubscriptionContext {
169
161
  */
170
162
  readonly timeSync: TimeSync;
171
163
 
172
- /**
173
- * Indicates whether the subscription is still live. Will be mutated to be
174
- * false when a subscription is
175
- */
176
- isSubscribed: boolean;
177
-
178
164
  /**
179
165
  * Indicates when the last time the subscription had its explicit interval
180
166
  * "satisfied".
@@ -183,7 +169,7 @@ export interface SubscriptionContext {
183
169
  * the active interval is set to fire every second, you may need to know
184
170
  * which update actually happened five minutes later.
185
171
  */
186
- intervalLastFulfilledAt: ReadonlyDate | null;
172
+ readonly intervalLastFulfilledAt: ReadonlyDate | null;
187
173
  }
188
174
 
189
175
  /**
@@ -203,8 +189,16 @@ interface TimeSyncApi {
203
189
  * intervals. Depending on how TimeSync is instantiated, it may choose to
204
190
  * de-duplicate these function calls on each round of updates.
205
191
  *
206
- * @throws {RangeError} If the provided interval is not either a positive
207
- * integer or positive infinity.
192
+ * If a value of Number.POSITIVE_INFINITY is used, the subscription will be
193
+ * considered "idle". Idle subscriptions cannot trigger updates on their
194
+ * own, but can stay in the loop as otherupdates get dispatched from via
195
+ * other subscriptions.
196
+ *
197
+ * Consider using the refreshRates object from this package for a set of
198
+ * commonly-used intervals.
199
+ *
200
+ * @throws {RangeError} If the provided interval is neither a positive
201
+ * integer nor positive infinity.
208
202
  * @returns An unsubscribe callback. Calling the callback more than once
209
203
  * results in a no-op.
210
204
  */
@@ -438,7 +432,7 @@ export class TimeSync implements TimeSyncApi {
438
432
  // first context in a sub array gets removed by unsubscribing, we
439
433
  // want what was the the second element to still be up to date
440
434
  let shouldCallOnUpdate = true;
441
- for (const context of subs) {
435
+ for (const ctx of subs as readonly Writeable<SubscriptionContext>[]) {
442
436
  // We're not doing anything more sophisticated here because
443
437
  // we're assuming that any systems that can clear out the
444
438
  // subscriptions will handle cleaning up each context, too
@@ -447,17 +441,15 @@ export class TimeSync implements TimeSyncApi {
447
441
  break outer;
448
442
  }
449
443
 
450
- const comparisonDate =
451
- context.intervalLastFulfilledAt ?? context.registeredAt;
444
+ const comparisonDate = ctx.intervalLastFulfilledAt ?? ctx.registeredAt;
452
445
  const isIntervalMatch =
453
- dateTime - comparisonDate.getTime() >=
454
- context.targetRefreshIntervalMs;
446
+ dateTime - comparisonDate.getTime() >= ctx.targetRefreshIntervalMs;
455
447
  if (isIntervalMatch) {
456
- context.intervalLastFulfilledAt = date;
448
+ ctx.intervalLastFulfilledAt = date;
457
449
  }
458
450
 
459
451
  if (shouldCallOnUpdate) {
460
- onUpdate(date, context);
452
+ onUpdate(date, ctx);
461
453
  shouldCallOnUpdate = config.allowDuplicateOnUpdateCalls;
462
454
  }
463
455
  }
@@ -585,7 +577,6 @@ export class TimeSync implements TimeSyncApi {
585
577
  // Have to define this as a writeable to avoid a chicken-and-the-egg
586
578
  // problem for the unsubscribe callback
587
579
  const context: Writeable<SubscriptionContext> = {
588
- isSubscribed: true,
589
580
  timeSync: this,
590
581
  unsubscribe: noOp,
591
582
  registeredAt: new ReadonlyDate(),
@@ -603,7 +594,6 @@ export class TimeSync implements TimeSyncApi {
603
594
  const subsOnSetup = this.#subscriptions;
604
595
  const unsubscribe = (): void => {
605
596
  if (!subscribed || this.#subscriptions !== subsOnSetup) {
606
- context.isSubscribed = false;
607
597
  subscribed = false;
608
598
  return;
609
599
  }
@@ -630,7 +620,6 @@ export class TimeSync implements TimeSyncApi {
630
620
  subscriberCount: Math.max(0, this.#latestSnapshot.subscriberCount - 1),
631
621
  });
632
622
 
633
- context.isSubscribed = false;
634
623
  subscribed = false;
635
624
  };
636
625
  context.unsubscribe = unsubscribe;
@@ -670,12 +659,6 @@ export class TimeSync implements TimeSyncApi {
670
659
  // As long as we clean things the internal state, it's safe not to
671
660
  // bother calling each unsubscribe callback. Not calling them one by
672
661
  // one actually has much better time complexity
673
- for (const subArray of this.#subscriptions.values()) {
674
- for (const ctx of subArray) {
675
- ctx.isSubscribed = false;
676
- }
677
- }
678
-
679
662
  this.#subscriptions.clear();
680
663
 
681
664
  // We swap the map out so that the unsubscribe callbacks can detect