@buenos-nachos/time-sync 0.1.1 → 0.2.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,21 @@
1
1
  # @buenos-nachos/time-sync
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Breaking Changes
6
+
7
+ - 2f527dd: Changed the default value of `allowDuplicateFunctionCalls` from `false` to `true`
8
+
9
+ ### Minor Changes
10
+
11
+ - 5f86fac: Added second parameter to `onUpdate` callback. This value is a value of type `SubscriptionContext` and provides information about the current subscription.
12
+
13
+ ## 0.1.2
14
+
15
+ ### Patch Changes
16
+
17
+ - 6189eb2: add README to root directory
18
+
3
19
  ## 0.1.1
4
20
 
5
21
  ### Patch Changes
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # `time-sync`
2
+
3
+ This package is in early development. Please see [the main repo for more information](https://www.github.com/buenos-nachos/time-sync).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buenos-nachos/time-sync",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "author": "Michael Smith <hello@nachos.dev> (https://www.nachos.dev)",
@@ -8,6 +8,11 @@
8
8
  "publishConfig": {
9
9
  "access": "public"
10
10
  },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/buenos-nachos/time-sync.git",
14
+ "directory": "packages/time-sync"
15
+ },
11
16
  "keywords": [
12
17
  "typescript",
13
18
  "utility",
@@ -4,8 +4,10 @@ import {
4
4
  type Configuration,
5
5
  refreshRates,
6
6
  type Snapshot,
7
+ type SubscriptionContext,
7
8
  TimeSync,
8
9
  } from "./TimeSync";
10
+ import type { Writeable } from "./utilities";
9
11
 
10
12
  const invalidIntervals: readonly number[] = [
11
13
  Number.NaN,
@@ -116,17 +118,26 @@ describe(TimeSync, () => {
116
118
  const sync = new TimeSync();
117
119
  const onUpdate = vi.fn();
118
120
 
119
- void sync.subscribe({
121
+ const dateBefore = sync.getStateSnapshot().date;
122
+ const unsubscribe = sync.subscribe({
120
123
  onUpdate,
121
124
  targetRefreshIntervalMs: rate,
122
125
  });
123
126
  expect(onUpdate).not.toHaveBeenCalled();
124
127
 
125
- const dateBefore = sync.getStateSnapshot().date;
126
128
  await vi.advanceTimersByTimeAsync(rate);
127
129
  const dateAfter = sync.getStateSnapshot().date;
128
130
  expect(onUpdate).toHaveBeenCalledTimes(1);
129
- expect(onUpdate).toHaveBeenCalledWith(dateAfter);
131
+
132
+ const expectedCtx: SubscriptionContext = {
133
+ unsubscribe,
134
+ isLive: true,
135
+ timeSync: sync,
136
+ intervalLastFulfilledAt: dateAfter,
137
+ registeredAt: dateBefore,
138
+ targetRefreshIntervalMs: rate,
139
+ };
140
+ expect(onUpdate).toHaveBeenCalledWith(dateAfter, expectedCtx);
130
141
 
131
142
  const diff = dateAfter.getTime() - dateBefore.getTime();
132
143
  expect(diff).toBe(rate);
@@ -171,7 +182,7 @@ describe(TimeSync, () => {
171
182
  it("Always dispatches updates in the order that callbacks were first registered", async ({
172
183
  expect,
173
184
  }) => {
174
- const sync = new TimeSync();
185
+ const sync = new TimeSync({ allowDuplicateOnUpdateCalls: false });
175
186
  const callOrder: number[] = [];
176
187
 
177
188
  const onUpdate1 = vi.fn(() => {
@@ -260,7 +271,7 @@ describe(TimeSync, () => {
260
271
  expect(secondOnUpdate).toHaveBeenCalledTimes(1);
261
272
  });
262
273
 
263
- it("Calls onUpdate callback one time total if callback is registered multiple times for the same time interval", async ({
274
+ it("Calls onUpdate callback once for each subscription (even if the same callback was registered multiple times)", async ({
264
275
  expect,
265
276
  }) => {
266
277
  const sync = new TimeSync();
@@ -274,59 +285,7 @@ describe(TimeSync, () => {
274
285
  }
275
286
 
276
287
  await vi.advanceTimersByTimeAsync(refreshRates.oneMinute);
277
- expect(sharedOnUpdate).toHaveBeenCalledTimes(1);
278
- });
279
-
280
- it("Calls onUpdate callback one time total if callback is registered multiple times for different time intervals", async ({
281
- expect,
282
- }) => {
283
- const sync = new TimeSync();
284
- const sharedOnUpdate = vi.fn();
285
-
286
- void sync.subscribe({
287
- onUpdate: sharedOnUpdate,
288
- targetRefreshIntervalMs: refreshRates.oneHour,
289
- });
290
- void sync.subscribe({
291
- onUpdate: sharedOnUpdate,
292
- targetRefreshIntervalMs: refreshRates.oneMinute,
293
- });
294
- void sync.subscribe({
295
- onUpdate: sharedOnUpdate,
296
- targetRefreshIntervalMs: refreshRates.oneSecond,
297
- });
298
-
299
- // Testing like this to ensure that for really, really long spans of
300
- // time, the no duplicated calls logic still holds up
301
- await vi.advanceTimersByTimeAsync(refreshRates.oneHour);
302
- const secondsInOneHour = 3600;
303
- expect(sharedOnUpdate).toHaveBeenCalledTimes(secondsInOneHour);
304
- });
305
-
306
- it("Calls onUpdate callback one time total if callback is registered multiple times with a mix of redundant/different intervals", async ({
307
- expect,
308
- }) => {
309
- const sync = new TimeSync();
310
- const sharedOnUpdate = vi.fn();
311
-
312
- for (let i = 0; i < 10; i++) {
313
- void sync.subscribe({
314
- onUpdate: sharedOnUpdate,
315
- targetRefreshIntervalMs: refreshRates.oneHour,
316
- });
317
- void sync.subscribe({
318
- onUpdate: sharedOnUpdate,
319
- targetRefreshIntervalMs: refreshRates.oneMinute,
320
- });
321
- void sync.subscribe({
322
- onUpdate: sharedOnUpdate,
323
- targetRefreshIntervalMs: refreshRates.oneSecond,
324
- });
325
- }
326
-
327
- await vi.advanceTimersByTimeAsync(refreshRates.oneHour);
328
- const secondsInOneHour = 3600;
329
- expect(sharedOnUpdate).toHaveBeenCalledTimes(secondsInOneHour);
288
+ expect(sharedOnUpdate).toHaveBeenCalledTimes(3);
330
289
  });
331
290
 
332
291
  it("Lets an external system unsubscribe", async ({ expect }) => {
@@ -405,7 +364,7 @@ describe(TimeSync, () => {
405
364
  const snap2 = sync.getStateSnapshot();
406
365
  expect(snap2.subscriberCount).toBe(30);
407
366
  await vi.advanceTimersByTimeAsync(refreshRates.oneSecond);
408
- expect(sharedOnUpdate).toHaveBeenCalledTimes(1);
367
+ expect(sharedOnUpdate).toHaveBeenCalledTimes(30);
409
368
  });
410
369
 
411
370
  it("Speeds up interval when new subscriber is added that is faster than all other subscribers", async ({
@@ -604,6 +563,132 @@ describe(TimeSync, () => {
604
563
  const dateAfter = sync.getStateSnapshot().date;
605
564
  expect(dateAfter).not.toEqual(dateBefore);
606
565
  });
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?.isLive).toBe(true);
584
+
585
+ unsub();
586
+ expect(ejectedContext?.isLive).toBe(false);
587
+ });
588
+ });
589
+
590
+ describe("Subscriptions: context values", () => {
591
+ it("Defaults to exposing the TimeSync instance the subscription was registered with", async ({
592
+ expect,
593
+ }) => {
594
+ const sync = new TimeSync();
595
+
596
+ let ejectedSync: TimeSync | null = null;
597
+ const onUpdate = vi.fn((_: unknown, ctx: SubscriptionContext) => {
598
+ ejectedSync = ctx.timeSync;
599
+ });
600
+
601
+ void sync.subscribe({
602
+ onUpdate,
603
+ targetRefreshIntervalMs: refreshRates.oneMinute,
604
+ });
605
+
606
+ await vi.advanceTimersByTimeAsync(refreshRates.oneMinute);
607
+ expect(ejectedSync).toBe(sync);
608
+ });
609
+
610
+ it("Exposes refresh interval used on initialization", async ({
611
+ expect,
612
+ }) => {
613
+ const sync = new TimeSync();
614
+ const interval = refreshRates.oneMinute;
615
+
616
+ let ejectedInterval: number | undefined;
617
+ const onUpdate = vi.fn((_: unknown, ctx: SubscriptionContext) => {
618
+ ejectedInterval = ctx.targetRefreshIntervalMs;
619
+ });
620
+
621
+ void sync.subscribe({
622
+ onUpdate,
623
+ targetRefreshIntervalMs: interval,
624
+ });
625
+
626
+ await vi.advanceTimersByTimeAsync(interval);
627
+ expect(ejectedInterval).toBe(interval);
628
+ });
629
+
630
+ it("Exposes exact same unsubscribe callback as the one returned from the subscribe call", async ({
631
+ expect,
632
+ }) => {
633
+ const sync = new TimeSync();
634
+
635
+ let ejectedUnsub: (() => void) | undefined;
636
+ const onUpdate = vi.fn((_: unknown, ctx: SubscriptionContext) => {
637
+ ejectedUnsub = ctx.unsubscribe;
638
+ });
639
+
640
+ const unsub = sync.subscribe({
641
+ onUpdate,
642
+ targetRefreshIntervalMs: refreshRates.oneMinute,
643
+ });
644
+
645
+ await vi.advanceTimersByTimeAsync(refreshRates.oneMinute);
646
+ expect(ejectedUnsub).toBe(unsub);
647
+ });
648
+
649
+ it("Exposes when the subscription was first set up", async ({ expect }) => {
650
+ const sync = new TimeSync();
651
+ const start = sync.getStateSnapshot().date;
652
+
653
+ let ejectedDate: Date | undefined;
654
+ const onUpdate = vi.fn((_: unknown, ctx: SubscriptionContext) => {
655
+ ejectedDate = ctx.registeredAt;
656
+ });
657
+
658
+ void sync.subscribe({
659
+ onUpdate,
660
+ targetRefreshIntervalMs: refreshRates.oneMinute,
661
+ });
662
+
663
+ await vi.advanceTimersByTimeAsync(refreshRates.oneMinute);
664
+ expect(ejectedDate).toEqual(start);
665
+ });
666
+
667
+ it("Indicates when the last requested interval was fulfilled", async ({
668
+ expect,
669
+ }) => {
670
+ const sync = new TimeSync();
671
+
672
+ const fulfilledValues: (ReadonlyDate | null)[] = [];
673
+ const onUpdate = vi.fn((_: unknown, ctx: SubscriptionContext) => {
674
+ fulfilledValues.push(ctx.intervalLastFulfilledAt);
675
+ });
676
+
677
+ void sync.subscribe({
678
+ onUpdate,
679
+ targetRefreshIntervalMs: refreshRates.oneMinute,
680
+ });
681
+ void sync.subscribe({
682
+ onUpdate: vi.fn(),
683
+ targetRefreshIntervalMs: refreshRates.thirtySeconds,
684
+ });
685
+
686
+ await vi.advanceTimersByTimeAsync(refreshRates.oneMinute);
687
+ const snapAfter = sync.getStateSnapshot().date;
688
+
689
+ expect(onUpdate).toHaveBeenCalledTimes(2);
690
+ expect(fulfilledValues).toEqual([null, snapAfter]);
691
+ });
607
692
  });
608
693
 
609
694
  describe("Subscriptions: custom `minimumRefreshIntervalMs` value", () => {
@@ -646,11 +731,14 @@ describe(TimeSync, () => {
646
731
  });
647
732
  });
648
733
 
649
- describe("Subscriptions: duplicating function calls", () => {
650
- it("Defaults to de-duplicating", async ({ expect }) => {
651
- const sync = new TimeSync();
734
+ describe("Subscriptions: turning off duplicate function calls", () => {
735
+ it("Calls onUpdate callback once for each subscription (even if the same callback was registered multiple times)", async ({
736
+ expect,
737
+ }) => {
738
+ const sync = new TimeSync({ allowDuplicateOnUpdateCalls: false });
652
739
  const sharedOnUpdate = vi.fn();
653
- for (let i = 0; i < 100; i++) {
740
+
741
+ for (let i = 1; i <= 3; i++) {
654
742
  void sync.subscribe({
655
743
  onUpdate: sharedOnUpdate,
656
744
  targetRefreshIntervalMs: refreshRates.oneMinute,
@@ -661,21 +749,104 @@ describe(TimeSync, () => {
661
749
  expect(sharedOnUpdate).toHaveBeenCalledTimes(1);
662
750
  });
663
751
 
664
- it("Lets user turn on duplication", async ({ expect }) => {
665
- const sync = new TimeSync({
666
- allowDuplicateOnUpdateCalls: true,
752
+ it("Calls onUpdate callback one time total if callback is registered multiple times for different time intervals", async ({
753
+ expect,
754
+ }) => {
755
+ const sync = new TimeSync({ allowDuplicateOnUpdateCalls: false });
756
+ const sharedOnUpdate = vi.fn();
757
+
758
+ void sync.subscribe({
759
+ onUpdate: sharedOnUpdate,
760
+ targetRefreshIntervalMs: refreshRates.oneHour,
761
+ });
762
+ void sync.subscribe({
763
+ onUpdate: sharedOnUpdate,
764
+ targetRefreshIntervalMs: refreshRates.oneMinute,
765
+ });
766
+ void sync.subscribe({
767
+ onUpdate: sharedOnUpdate,
768
+ targetRefreshIntervalMs: refreshRates.oneSecond,
667
769
  });
668
770
 
771
+ // Testing like this to ensure that for really, really long spans of
772
+ // time, the no duplicated calls logic still holds up
773
+ await vi.advanceTimersByTimeAsync(refreshRates.oneHour);
774
+ const secondsInOneHour = 3600;
775
+ expect(sharedOnUpdate).toHaveBeenCalledTimes(secondsInOneHour);
776
+ });
777
+
778
+ it("Calls onUpdate callback one time total if callback is registered multiple times with a mix of redundant/different intervals", async ({
779
+ expect,
780
+ }) => {
781
+ const sync = new TimeSync({ allowDuplicateOnUpdateCalls: false });
669
782
  const sharedOnUpdate = vi.fn();
670
- for (let i = 0; i < 100; i++) {
783
+
784
+ for (let i = 0; i < 10; i++) {
785
+ void sync.subscribe({
786
+ onUpdate: sharedOnUpdate,
787
+ targetRefreshIntervalMs: refreshRates.oneHour,
788
+ });
671
789
  void sync.subscribe({
672
790
  onUpdate: sharedOnUpdate,
673
791
  targetRefreshIntervalMs: refreshRates.oneMinute,
674
792
  });
793
+ void sync.subscribe({
794
+ onUpdate: sharedOnUpdate,
795
+ targetRefreshIntervalMs: refreshRates.oneSecond,
796
+ });
675
797
  }
676
798
 
677
- await vi.advanceTimersByTimeAsync(refreshRates.oneMinute);
678
- expect(sharedOnUpdate).toHaveBeenCalledTimes(100);
799
+ await vi.advanceTimersByTimeAsync(refreshRates.oneHour);
800
+ const secondsInOneHour = 3600;
801
+ expect(sharedOnUpdate).toHaveBeenCalledTimes(secondsInOneHour);
802
+ });
803
+
804
+ it("Always exposes the context for the subscription that first set up onUpdate", async ({
805
+ expect,
806
+ }) => {
807
+ const sync = new TimeSync();
808
+ const snapBefore = sync.getStateSnapshot().date;
809
+
810
+ let ejectedContext: SubscriptionContext | undefined;
811
+ const onUpdate = vi.fn((_: unknown, ctx: SubscriptionContext) => {
812
+ ejectedContext = ctx;
813
+ });
814
+
815
+ const unsub = sync.subscribe({
816
+ onUpdate,
817
+ targetRefreshIntervalMs: refreshRates.oneHour,
818
+ });
819
+ void sync.subscribe({
820
+ onUpdate,
821
+ targetRefreshIntervalMs: refreshRates.oneMinute,
822
+ });
823
+ void sync.subscribe({
824
+ onUpdate,
825
+ targetRefreshIntervalMs: refreshRates.oneSecond,
826
+ });
827
+
828
+ await vi.advanceTimersByTimeAsync(refreshRates.oneSecond);
829
+ expect(ejectedContext).toEqual<SubscriptionContext>({
830
+ isLive: true,
831
+ intervalLastFulfilledAt: null,
832
+ registeredAt: snapBefore,
833
+ targetRefreshIntervalMs: refreshRates.oneHour,
834
+ timeSync: sync,
835
+ unsubscribe: unsub,
836
+ });
837
+
838
+ const remainingSecondsToOneHour = refreshRates.oneHour - 1000;
839
+ await vi.advanceTimersByTimeAsync(remainingSecondsToOneHour);
840
+
841
+ const snapAfter = sync.getStateSnapshot().date;
842
+ expect(ejectedContext).toEqual<SubscriptionContext>({
843
+ isLive: true,
844
+ intervalLastFulfilledAt: snapAfter,
845
+ registeredAt: snapBefore,
846
+ targetRefreshIntervalMs: refreshRates.oneHour,
847
+ timeSync: sync,
848
+ unsubscribe: unsub,
849
+ });
679
850
  });
680
851
  });
681
852
 
@@ -685,7 +856,11 @@ describe(TimeSync, () => {
685
856
  }) => {
686
857
  const initialDate = setInitialTime("July 4, 1999");
687
858
  const minimumRefreshIntervalMs = 5_000_000;
688
- const sync = new TimeSync({ initialDate, minimumRefreshIntervalMs });
859
+ const sync = new TimeSync({
860
+ initialDate,
861
+ minimumRefreshIntervalMs,
862
+ allowDuplicateOnUpdateCalls: false,
863
+ });
689
864
 
690
865
  const snap = sync.getStateSnapshot();
691
866
  expect(snap).toEqual<Snapshot>({
@@ -748,9 +923,8 @@ describe(TimeSync, () => {
748
923
  await vi.advanceTimersByTimeAsync(refreshRates.oneHour);
749
924
 
750
925
  expect(onUpdate).toHaveBeenCalledTimes(1);
751
- expect(onUpdate).toHaveBeenCalledWith(expect.any(Date));
752
-
753
926
  const newSnap = sync.getStateSnapshot();
927
+ expect(onUpdate).toHaveBeenCalledWith(newSnap.date, expect.any(Object));
754
928
  expect(newSnap).not.toEqual(initialSnap);
755
929
  });
756
930
 
@@ -818,7 +992,6 @@ describe(TimeSync, () => {
818
992
  });
819
993
 
820
994
  it("Prevents mutating properties at runtime", ({ expect }) => {
821
- type Writeable<T> = { -readonly [Key in keyof T]: T[Key] };
822
995
  const sync = new TimeSync();
823
996
 
824
997
  // We have readonly modifiers on the types, but we need to make sure
@@ -913,6 +1086,28 @@ describe(TimeSync, () => {
913
1086
  await vi.advanceTimersByTimeAsync(refreshRates.oneMinute);
914
1087
  expect(sharedOnUpdate).not.toHaveBeenCalled();
915
1088
  });
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?.isLive).toBe(true);
1107
+
1108
+ sync.clearAll();
1109
+ expect(ejectedContext?.isLive).toBe(false);
1110
+ });
916
1111
  });
917
1112
 
918
1113
  /**
@@ -1067,5 +1262,31 @@ describe(TimeSync, () => {
1067
1262
  const snap3 = sync.getStateSnapshot().subscriberCount;
1068
1263
  expect(snap3).toBe(0);
1069
1264
  });
1265
+
1266
+ it("Lets consumers detect whether an update corresponds to the subscription they explicitly set up", async ({
1267
+ expect,
1268
+ }) => {
1269
+ const sync = new TimeSync();
1270
+ const innerOnUpdate = vi.fn();
1271
+
1272
+ void sync.subscribe({
1273
+ targetRefreshIntervalMs: refreshRates.oneMinute,
1274
+ onUpdate: (date, ctx) => {
1275
+ const intervalMatches =
1276
+ date.getTime() === ctx.intervalLastFulfilledAt?.getTime();
1277
+ if (intervalMatches) {
1278
+ innerOnUpdate();
1279
+ }
1280
+ },
1281
+ });
1282
+
1283
+ void sync.subscribe({
1284
+ targetRefreshIntervalMs: refreshRates.thirtySeconds,
1285
+ onUpdate: vi.fn(),
1286
+ });
1287
+
1288
+ await vi.advanceTimersByTimeAsync(refreshRates.oneMinute);
1289
+ expect(innerOnUpdate).toHaveBeenCalledTimes(1);
1290
+ });
1070
1291
  });
1071
1292
  });