@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 +16 -0
- package/README.md +3 -0
- package/package.json +6 -1
- package/src/TimeSync.test.ts +294 -73
- package/src/TimeSync.ts +218 -115
- package/src/index.ts +2 -1
- package/src/utilities.ts +1 -0
- package/dist/index.d.mts +0 -275
- package/dist/index.d.mts.map +0 -1
- package/dist/index.d.ts +0 -275
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -409
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -406
- package/dist/index.mjs.map +0 -1
package/src/TimeSync.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ReadonlyDate } from "./ReadonlyDate";
|
|
2
|
+
import type { Writeable } from "./utilities";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* A collection of commonly-needed intervals (all defined in milliseconds).
|
|
@@ -26,14 +27,14 @@ export const refreshRates = Object.freeze({
|
|
|
26
27
|
/**
|
|
27
28
|
* The set of readonly options that the TimeSync has been configured with.
|
|
28
29
|
*/
|
|
29
|
-
export
|
|
30
|
+
export interface Configuration {
|
|
30
31
|
/**
|
|
31
32
|
* Indicates whether the TimeSync instance should be frozen for Snapshot
|
|
32
33
|
* tests.
|
|
33
34
|
*
|
|
34
35
|
* Defaults to false.
|
|
35
36
|
*/
|
|
36
|
-
freezeUpdates: boolean;
|
|
37
|
+
readonly freezeUpdates: boolean;
|
|
37
38
|
|
|
38
39
|
/**
|
|
39
40
|
* The minimum refresh interval (in milliseconds) to use when dispatching
|
|
@@ -48,49 +49,44 @@ export type Configuration = Readonly<{
|
|
|
48
49
|
*
|
|
49
50
|
* Defaults to 200ms.
|
|
50
51
|
*/
|
|
51
|
-
minimumRefreshIntervalMs: number;
|
|
52
|
+
readonly minimumRefreshIntervalMs: number;
|
|
52
53
|
|
|
53
54
|
/**
|
|
54
55
|
* Indicates whether the same `onUpdate` callback (by reference) should be
|
|
55
56
|
* called multiple time if registered by multiple systems.
|
|
56
57
|
*
|
|
57
|
-
* Defaults to false
|
|
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.
|
|
58
61
|
*/
|
|
59
|
-
allowDuplicateOnUpdateCalls: boolean;
|
|
60
|
-
}
|
|
62
|
+
readonly allowDuplicateOnUpdateCalls: boolean;
|
|
63
|
+
}
|
|
61
64
|
|
|
62
65
|
/**
|
|
63
66
|
* The set of options that can be used to instantiate a TimeSync.
|
|
64
67
|
*/
|
|
65
|
-
export
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
freezeUpdates: boolean;
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* The Date object to use when initializing TimeSync to make the
|
|
79
|
-
* constructor more pure and deterministic.
|
|
80
|
-
*/
|
|
81
|
-
initialDate: Date;
|
|
82
|
-
}
|
|
83
|
-
>;
|
|
68
|
+
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;
|
|
84
78
|
|
|
85
|
-
/**
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
79
|
+
/**
|
|
80
|
+
* The Date object to use when initializing TimeSync to make the
|
|
81
|
+
* constructor more pure and deterministic.
|
|
82
|
+
*/
|
|
83
|
+
readonly initialDate: Date;
|
|
84
|
+
}
|
|
89
85
|
|
|
90
86
|
/**
|
|
91
87
|
* An object used to initialize a new subscription for TimeSync.
|
|
92
88
|
*/
|
|
93
|
-
export
|
|
89
|
+
export interface SubscriptionInitOptions {
|
|
94
90
|
/**
|
|
95
91
|
* The maximum update interval that a subscriber needs. A value of
|
|
96
92
|
* Number.POSITIVE_INFINITY indicates that the subscriber does not strictly
|
|
@@ -110,36 +106,93 @@ export type SubscriptionOptions = Readonly<{
|
|
|
110
106
|
* after A, updates will pause completely until a new subscriber gets
|
|
111
107
|
* added, and it has a non-infinite interval.
|
|
112
108
|
*/
|
|
113
|
-
targetRefreshIntervalMs: number;
|
|
109
|
+
readonly targetRefreshIntervalMs: number;
|
|
114
110
|
|
|
115
111
|
/**
|
|
116
112
|
* The callback to call when a new state update needs to be flushed amongst
|
|
117
113
|
* all subscribers.
|
|
118
114
|
*/
|
|
119
|
-
onUpdate: OnTimeSyncUpdate;
|
|
120
|
-
}
|
|
115
|
+
readonly onUpdate: OnTimeSyncUpdate;
|
|
116
|
+
}
|
|
121
117
|
|
|
122
118
|
/**
|
|
123
119
|
* A complete snapshot of the user-relevant internal state from TimeSync. This
|
|
124
120
|
* value is treated as immutable at both runtime and compile time.
|
|
125
121
|
*/
|
|
126
|
-
export
|
|
122
|
+
export interface Snapshot {
|
|
127
123
|
/**
|
|
128
124
|
* The date that was last dispatched to all subscribers.
|
|
129
125
|
*/
|
|
130
|
-
date: ReadonlyDate;
|
|
126
|
+
readonly date: ReadonlyDate;
|
|
131
127
|
|
|
132
128
|
/**
|
|
133
129
|
* The number of subscribers registered with TimeSync.
|
|
134
130
|
*/
|
|
135
|
-
subscriberCount: number;
|
|
131
|
+
readonly subscriberCount: number;
|
|
136
132
|
|
|
137
133
|
/**
|
|
138
134
|
* The configuration options used when instantiating the TimeSync instance.
|
|
139
135
|
* The value is guaranteed to be stable for the entire lifetime of TimeSync.
|
|
140
136
|
*/
|
|
141
|
-
config: Configuration;
|
|
142
|
-
}
|
|
137
|
+
readonly config: Configuration;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* An object with information about a specific subscription registered with
|
|
142
|
+
* TimeSync.
|
|
143
|
+
*
|
|
144
|
+
* 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.
|
|
148
|
+
*/
|
|
149
|
+
export interface SubscriptionContext {
|
|
150
|
+
/**
|
|
151
|
+
* The interval that the subscription was registered with.
|
|
152
|
+
*/
|
|
153
|
+
readonly targetRefreshIntervalMs: number;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The unsubscribe callback associated with a subscription. This is the same
|
|
157
|
+
* callback returned by `TimeSync.subscribe`.
|
|
158
|
+
*/
|
|
159
|
+
readonly unsubscribe: () => void;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* A timestamp of when the subscription was first set up.
|
|
163
|
+
*/
|
|
164
|
+
readonly registeredAt: ReadonlyDate;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* A reference to the TimeSync instance that the subscription was registered
|
|
168
|
+
* with.
|
|
169
|
+
*/
|
|
170
|
+
timeSync: TimeSync | null;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Indicates whether the subscription is still live. Will be mutated to be
|
|
174
|
+
* false when a subscription is
|
|
175
|
+
*/
|
|
176
|
+
isLive: boolean;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Indicates when the last time the subscription had its explicit interval
|
|
180
|
+
* "satisfied".
|
|
181
|
+
*
|
|
182
|
+
* For example, if a subscription is registered for every five minutes, but
|
|
183
|
+
* the active interval is set to fire every second, you may need to know
|
|
184
|
+
* which update actually happened five minutes later.
|
|
185
|
+
*/
|
|
186
|
+
intervalLastFulfilledAt: ReadonlyDate | null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The callback to call when a new state update is ready to be dispatched.
|
|
191
|
+
*/
|
|
192
|
+
export type OnTimeSyncUpdate = (
|
|
193
|
+
newDate: ReadonlyDate,
|
|
194
|
+
context: SubscriptionContext,
|
|
195
|
+
) => void;
|
|
143
196
|
|
|
144
197
|
interface TimeSyncApi {
|
|
145
198
|
/**
|
|
@@ -155,7 +208,7 @@ interface TimeSyncApi {
|
|
|
155
208
|
* @returns An unsubscribe callback. Calling the callback more than once
|
|
156
209
|
* results in a no-op.
|
|
157
210
|
*/
|
|
158
|
-
subscribe: (options:
|
|
211
|
+
subscribe: (options: SubscriptionInitOptions) => () => void;
|
|
159
212
|
|
|
160
213
|
/**
|
|
161
214
|
* Allows an external system to pull an immutable snapshot of some of the
|
|
@@ -179,11 +232,6 @@ interface TimeSyncApi {
|
|
|
179
232
|
clearAll: () => void;
|
|
180
233
|
}
|
|
181
234
|
|
|
182
|
-
type SubscriptionEntry = Readonly<{
|
|
183
|
-
targetInterval: number;
|
|
184
|
-
unsubscribe: () => void;
|
|
185
|
-
}>;
|
|
186
|
-
|
|
187
235
|
/* biome-ignore lint:suspicious/noEmptyBlockStatements -- Rare case where we do
|
|
188
236
|
actually want a completely empty function body. */
|
|
189
237
|
function noOp(..._: readonly unknown[]): void {}
|
|
@@ -238,8 +286,27 @@ export class TimeSync implements TimeSyncApi {
|
|
|
238
286
|
*
|
|
239
287
|
* Each map value should stay sorted by refresh interval, in ascending
|
|
240
288
|
* order.
|
|
289
|
+
*
|
|
290
|
+
* ---
|
|
291
|
+
*
|
|
292
|
+
* This is a rare case where we actually REALLY need the readonly modifier
|
|
293
|
+
* to avoid infinite loops. JavaScript's iterator protocol is really great
|
|
294
|
+
* for making loops simple and type-safe, but because subscriptions have the
|
|
295
|
+
* ability to add more subscriptions, we need to make an immutable version
|
|
296
|
+
* of each array at some point to make sure that we're not iterating through
|
|
297
|
+
* values forever
|
|
298
|
+
*
|
|
299
|
+
* We can choose to do that at one of two points:
|
|
300
|
+
* 1. When adding a new subscription
|
|
301
|
+
* 2. When dispatching a new round of updates
|
|
302
|
+
*
|
|
303
|
+
* Because this library assumes that dispatches will be much more common
|
|
304
|
+
* than new subscriptions (a single subscription that subscribes for one
|
|
305
|
+
* second will receive 360 updates in five minutes), operations should be
|
|
306
|
+
* done to optimize that use case. So we should move the immutability costs
|
|
307
|
+
* to the subscribe and unsubscribe operations.
|
|
241
308
|
*/
|
|
242
|
-
#subscriptions: Map<OnTimeSyncUpdate,
|
|
309
|
+
#subscriptions: Map<OnTimeSyncUpdate, readonly SubscriptionContext[]>;
|
|
243
310
|
|
|
244
311
|
/**
|
|
245
312
|
* The latest public snapshot of TimeSync's internal state. The snapshot
|
|
@@ -271,7 +338,7 @@ export class TimeSync implements TimeSyncApi {
|
|
|
271
338
|
const {
|
|
272
339
|
initialDate,
|
|
273
340
|
freezeUpdates = false,
|
|
274
|
-
allowDuplicateOnUpdateCalls =
|
|
341
|
+
allowDuplicateOnUpdateCalls = true,
|
|
275
342
|
minimumRefreshIntervalMs = defaultMinimumRefreshIntervalMs,
|
|
276
343
|
} = options ?? {};
|
|
277
344
|
|
|
@@ -343,50 +410,57 @@ export class TimeSync implements TimeSyncApi {
|
|
|
343
410
|
return;
|
|
344
411
|
}
|
|
345
412
|
|
|
413
|
+
const dateTime = date.getTime();
|
|
414
|
+
|
|
346
415
|
/**
|
|
347
|
-
* Two things
|
|
348
|
-
* 1.
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
416
|
+
* Two things:
|
|
417
|
+
* 1. Even though the context arrays are defined as readonly (which
|
|
418
|
+
* removes on the worst edge cases during dispatching), the
|
|
419
|
+
* subscriptions map itself is still mutable, so there are a few edge
|
|
420
|
+
* cases we need to deal with. While the risk of infinite loops should
|
|
421
|
+
* be much lower, there's still the risk that an onUpdate callback could
|
|
422
|
+
* add a subscriber for an interval that wasn't registered before, which
|
|
423
|
+
* the iterator protocol will pick up. Need to make a local,
|
|
424
|
+
* fixed-length copy of the map entries before starting iteration. Any
|
|
425
|
+
* subscriptions added during update will just have to wait until the
|
|
426
|
+
* next round of updates.
|
|
354
427
|
*
|
|
355
428
|
* 2. The trade off of the serialization is that we do lose the ability
|
|
356
|
-
* to auto-break the
|
|
429
|
+
* to auto-break the loop if one of the subscribers ends up resetting
|
|
357
430
|
* all state, because we'll still have local copies of entries. We need
|
|
358
431
|
* to check on each iteration to see if we should continue.
|
|
359
432
|
*/
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
433
|
+
const subsBeforeUpdate = this.#subscriptions;
|
|
434
|
+
const entries = Array.from(subsBeforeUpdate);
|
|
435
|
+
outer: for (const [onUpdate, subs] of entries) {
|
|
436
|
+
// Even if duplicate onUpdate calls are disabled, we still need to
|
|
437
|
+
// iterate through everything and update any internal data. If the
|
|
438
|
+
// first context in a sub array gets removed by unsubscribing, we
|
|
439
|
+
// want what was the the second element to still be up to date
|
|
440
|
+
let shouldCallOnUpdate = true;
|
|
441
|
+
for (const context of subs) {
|
|
442
|
+
// We're not doing anything more sophisticated here because
|
|
443
|
+
// we're assuming that any systems that can clear out the
|
|
444
|
+
// subscriptions will handle cleaning up each context, too
|
|
445
|
+
const wasCleared = subsBeforeUpdate.size === 0;
|
|
446
|
+
if (wasCleared) {
|
|
447
|
+
break outer;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const comparisonDate =
|
|
451
|
+
context.intervalLastFulfilledAt ?? context.registeredAt;
|
|
452
|
+
const isIntervalMatch =
|
|
453
|
+
dateTime - comparisonDate.getTime() >=
|
|
454
|
+
context.targetRefreshIntervalMs;
|
|
455
|
+
if (isIntervalMatch) {
|
|
456
|
+
context.intervalLastFulfilledAt = date;
|
|
378
457
|
}
|
|
379
|
-
}
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
458
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
break;
|
|
459
|
+
if (shouldCallOnUpdate) {
|
|
460
|
+
onUpdate(date, context);
|
|
461
|
+
shouldCallOnUpdate = config.allowDuplicateOnUpdateCalls;
|
|
462
|
+
}
|
|
388
463
|
}
|
|
389
|
-
onUpdate(date);
|
|
390
464
|
}
|
|
391
465
|
}
|
|
392
466
|
|
|
@@ -475,7 +549,8 @@ export class TimeSync implements TimeSyncApi {
|
|
|
475
549
|
// This setup requires that every interval array stay sorted. It
|
|
476
550
|
// immediately falls apart if this isn't guaranteed.
|
|
477
551
|
for (const entries of this.#subscriptions.values()) {
|
|
478
|
-
const subFastest =
|
|
552
|
+
const subFastest =
|
|
553
|
+
entries[0]?.targetRefreshIntervalMs ?? Number.POSITIVE_INFINITY;
|
|
479
554
|
if (subFastest < newFastest) {
|
|
480
555
|
newFastest = subFastest;
|
|
481
556
|
}
|
|
@@ -487,7 +562,7 @@ export class TimeSync implements TimeSyncApi {
|
|
|
487
562
|
}
|
|
488
563
|
}
|
|
489
564
|
|
|
490
|
-
subscribe(
|
|
565
|
+
subscribe(options: SubscriptionInitOptions): () => void {
|
|
491
566
|
const { config } = this.#latestSnapshot;
|
|
492
567
|
if (config.freezeUpdates) {
|
|
493
568
|
return noOp;
|
|
@@ -495,7 +570,7 @@ export class TimeSync implements TimeSyncApi {
|
|
|
495
570
|
|
|
496
571
|
// Destructuring properties so that they can't be fiddled with after
|
|
497
572
|
// this function call ends
|
|
498
|
-
const { targetRefreshIntervalMs, onUpdate } =
|
|
573
|
+
const { targetRefreshIntervalMs, onUpdate } = options;
|
|
499
574
|
|
|
500
575
|
const isTargetValid =
|
|
501
576
|
targetRefreshIntervalMs === Number.POSITIVE_INFINITY ||
|
|
@@ -507,50 +582,73 @@ export class TimeSync implements TimeSyncApi {
|
|
|
507
582
|
);
|
|
508
583
|
}
|
|
509
584
|
|
|
510
|
-
|
|
585
|
+
// Have to define this as a writeable to avoid a chicken-and-the-egg
|
|
586
|
+
// problem for the unsubscribe callback
|
|
587
|
+
const context: Writeable<SubscriptionContext> = {
|
|
588
|
+
isLive: true,
|
|
589
|
+
timeSync: this,
|
|
590
|
+
unsubscribe: noOp,
|
|
591
|
+
registeredAt: new ReadonlyDate(),
|
|
592
|
+
intervalLastFulfilledAt: null,
|
|
593
|
+
targetRefreshIntervalMs: Math.max(
|
|
594
|
+
config.minimumRefreshIntervalMs,
|
|
595
|
+
targetRefreshIntervalMs,
|
|
596
|
+
),
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
// Not reading from context value to decide whether to bail out of
|
|
600
|
+
// unsubscribes in off chance that outside consumer accidentally mutates
|
|
601
|
+
// the value
|
|
511
602
|
let subscribed = true;
|
|
603
|
+
const subsOnSetup = this.#subscriptions;
|
|
512
604
|
const unsubscribe = (): void => {
|
|
513
605
|
if (!subscribed || this.#subscriptions !== subsOnSetup) {
|
|
606
|
+
context.isLive = false;
|
|
514
607
|
subscribed = false;
|
|
515
608
|
return;
|
|
516
609
|
}
|
|
517
610
|
|
|
518
|
-
const
|
|
519
|
-
if (
|
|
611
|
+
const contexts = subsOnSetup.get(onUpdate);
|
|
612
|
+
if (contexts === undefined) {
|
|
520
613
|
return;
|
|
521
614
|
}
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
);
|
|
525
|
-
if (matchIndex === -1) {
|
|
615
|
+
const filtered = contexts.filter((e) => e.unsubscribe !== unsubscribe);
|
|
616
|
+
if (filtered.length === contexts.length) {
|
|
526
617
|
return;
|
|
527
618
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
entries.splice(matchIndex, 1);
|
|
531
|
-
if (entries.length === 0) {
|
|
619
|
+
|
|
620
|
+
if (filtered.length === 0) {
|
|
532
621
|
subsOnSetup.delete(onUpdate);
|
|
533
622
|
this.#updateFastestInterval();
|
|
623
|
+
} else {
|
|
624
|
+
// No need to sort on removal because everything gets sorted as
|
|
625
|
+
// it enters the subscriptions map
|
|
626
|
+
subsOnSetup.set(onUpdate, filtered);
|
|
534
627
|
}
|
|
535
628
|
|
|
536
629
|
void this.#setSnapshot({
|
|
537
630
|
subscriberCount: Math.max(0, this.#latestSnapshot.subscriberCount - 1),
|
|
538
631
|
});
|
|
632
|
+
|
|
633
|
+
context.isLive = false;
|
|
539
634
|
subscribed = false;
|
|
540
635
|
};
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
636
|
+
context.unsubscribe = unsubscribe;
|
|
637
|
+
|
|
638
|
+
let contexts: SubscriptionContext[];
|
|
639
|
+
if (this.#subscriptions.has(onUpdate)) {
|
|
640
|
+
const prev = this.#subscriptions.get(onUpdate) as SubscriptionContext[];
|
|
641
|
+
contexts = [...prev];
|
|
642
|
+
} else {
|
|
643
|
+
contexts = [];
|
|
644
|
+
subsOnSetup.set(onUpdate, contexts);
|
|
546
645
|
}
|
|
547
646
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
647
|
+
subsOnSetup.set(onUpdate, contexts);
|
|
648
|
+
contexts.push(context);
|
|
649
|
+
contexts.sort(
|
|
650
|
+
(e1, e2) => e1.targetRefreshIntervalMs - e2.targetRefreshIntervalMs,
|
|
551
651
|
);
|
|
552
|
-
entries.push({ unsubscribe, targetInterval });
|
|
553
|
-
entries.sort((e1, e2) => e1.targetInterval - e2.targetInterval);
|
|
554
652
|
|
|
555
653
|
void this.#setSnapshot({
|
|
556
654
|
subscriberCount: this.#latestSnapshot.subscriberCount + 1,
|
|
@@ -567,17 +665,22 @@ export class TimeSync implements TimeSyncApi {
|
|
|
567
665
|
clearAll(): void {
|
|
568
666
|
clearInterval(this.#intervalId);
|
|
569
667
|
this.#intervalId = undefined;
|
|
570
|
-
this.#fastestRefreshInterval =
|
|
571
|
-
|
|
572
|
-
//
|
|
573
|
-
//
|
|
574
|
-
//
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
668
|
+
this.#fastestRefreshInterval = Number.POSITIVE_INFINITY;
|
|
669
|
+
|
|
670
|
+
// As long as we clean things the internal state, it's safe not to
|
|
671
|
+
// bother calling each unsubscribe callback. Not calling them one by
|
|
672
|
+
// one actually has much better time complexity
|
|
673
|
+
for (const subArray of this.#subscriptions.values()) {
|
|
674
|
+
for (const ctx of subArray) {
|
|
675
|
+
ctx.isLive = false;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
this.#subscriptions.clear();
|
|
680
|
+
|
|
681
|
+
// We swap the map out so that the unsubscribe callbacks can detect
|
|
682
|
+
// whether their functionality is still relevant
|
|
579
683
|
this.#subscriptions = new Map();
|
|
580
|
-
subsBefore.clear();
|
|
581
684
|
void this.#setSnapshot({ subscriberCount: 0 });
|
|
582
685
|
}
|
|
583
686
|
}
|
package/src/index.ts
CHANGED
package/src/utilities.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type Writeable<T> = { -readonly [Key in keyof T]: T[Key] };
|