oidc-spa 4.2.1 → 4.3.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/README.md +1 -0
- package/oidc.d.ts +14 -0
- package/oidc.js +124 -35
- package/oidc.js.map +1 -1
- package/package.json +26 -1
- package/react.d.ts +7 -1
- package/react.js +1 -1
- package/react.js.map +1 -1
- package/src/oidc.ts +138 -16
- package/src/react.tsx +9 -2
- package/src/tools/StatefulObservable.ts +52 -0
- package/src/tools/createIsUserActive.ts +41 -0
- package/src/tools/getPrUserInteraction.ts +23 -0
- package/src/tools/startCountdown.ts +35 -0
- package/src/tools/subscribeToUserInteraction.ts +33 -0
- package/tools/StatefulObservable.d.ts +12 -0
- package/tools/StatefulObservable.js +34 -0
- package/tools/StatefulObservable.js.map +1 -0
- package/tools/createIsUserActive.d.ts +6 -0
- package/tools/createIsUserActive.js +38 -0
- package/tools/createIsUserActive.js.map +1 -0
- package/tools/getPrUserInteraction.d.ts +4 -0
- package/tools/getPrUserInteraction.js +23 -0
- package/tools/getPrUserInteraction.js.map +1 -0
- package/tools/startCountdown.d.ts +10 -0
- package/tools/startCountdown.js +76 -0
- package/tools/startCountdown.js.map +1 -0
- package/tools/subscribeToUserInteraction.d.ts +6 -0
- package/tools/subscribeToUserInteraction.js +77 -0
- package/tools/subscribeToUserInteraction.js.map +1 -0
package/src/oidc.ts
CHANGED
|
@@ -7,6 +7,9 @@ import { fnv1aHashToHex } from "./tools/fnv1aHashToHex";
|
|
|
7
7
|
import { Deferred } from "./tools/Deferred";
|
|
8
8
|
import { decodeJwt } from "./tools/decodeJwt";
|
|
9
9
|
import { getDownlinkAndRtt } from "./tools/getDownlinkAndRtt";
|
|
10
|
+
import { createIsUserActive } from "./tools/createIsUserActive";
|
|
11
|
+
import { createStartCountdown } from "./tools/startCountdown";
|
|
12
|
+
import type { StatefulObservable } from "./tools/StatefulObservable";
|
|
10
13
|
|
|
11
14
|
export declare type Oidc<DecodedIdToken extends Record<string, unknown> = Record<string, unknown>> =
|
|
12
15
|
| Oidc.LoggedIn<DecodedIdToken>
|
|
@@ -40,6 +43,9 @@ export declare namespace Oidc {
|
|
|
40
43
|
| { redirectTo: "home" | "current page" }
|
|
41
44
|
| { redirectTo: "specific url"; url: string }
|
|
42
45
|
) => Promise<never>;
|
|
46
|
+
subscribeToAutoLogoutCountdown: (
|
|
47
|
+
tickCallback: (params: { secondsLeft: number | undefined }) => void
|
|
48
|
+
) => { unsubscribeFromAutoLogoutCountdown: () => void };
|
|
43
49
|
};
|
|
44
50
|
|
|
45
51
|
export type Tokens<DecodedIdToken extends Record<string, unknown> = Record<string, unknown>> =
|
|
@@ -120,8 +126,20 @@ export type ParamsOfCreateOidc<
|
|
|
120
126
|
*/
|
|
121
127
|
publicUrl?: string;
|
|
122
128
|
decodedIdTokenSchema?: { parse: (data: unknown) => DecodedIdToken };
|
|
129
|
+
/**
|
|
130
|
+
* This parameter defines after how many seconds of inactivity the user should be
|
|
131
|
+
* logged out automatically.
|
|
132
|
+
*
|
|
133
|
+
* WARNING: It should be configured on the identity server side
|
|
134
|
+
* as it's the authoritative source for security policies and not the client.
|
|
135
|
+
* If you don't provide this parameter it will be inferred from the refresh token expiration time.
|
|
136
|
+
* */
|
|
137
|
+
__unsafe_ssoSessionIdleSeconds?: number;
|
|
123
138
|
};
|
|
124
139
|
|
|
140
|
+
let $isUserActive: StatefulObservable<boolean> | undefined = undefined;
|
|
141
|
+
const hotReloadCleanups = new Map<string, Set<() => void>>();
|
|
142
|
+
|
|
125
143
|
/** @see: https://github.com/garronej/oidc-spa#option-1-usage-without-involving-the-ui-framework */
|
|
126
144
|
export async function createOidc<
|
|
127
145
|
DecodedIdToken extends Record<string, unknown> = Record<string, unknown>
|
|
@@ -133,7 +151,8 @@ export async function createOidc<
|
|
|
133
151
|
transformUrlBeforeRedirect = url => url,
|
|
134
152
|
extraQueryParams: extraQueryParamsOrGetter,
|
|
135
153
|
publicUrl: publicUrl_params,
|
|
136
|
-
decodedIdTokenSchema
|
|
154
|
+
decodedIdTokenSchema,
|
|
155
|
+
__unsafe_ssoSessionIdleSeconds
|
|
137
156
|
} = params;
|
|
138
157
|
|
|
139
158
|
const getExtraQueryParams = (() => {
|
|
@@ -161,6 +180,12 @@ export async function createOidc<
|
|
|
161
180
|
})();
|
|
162
181
|
|
|
163
182
|
const configHash = fnv1aHashToHex(`${issuerUri} ${clientId}`);
|
|
183
|
+
|
|
184
|
+
{
|
|
185
|
+
hotReloadCleanups.get(configHash)?.forEach(cleanup => cleanup());
|
|
186
|
+
hotReloadCleanups.set(configHash, new Set());
|
|
187
|
+
}
|
|
188
|
+
|
|
164
189
|
const configHashKey = "configHash";
|
|
165
190
|
|
|
166
191
|
const oidcClientTsUserManager = new OidcClientTsUserManager({
|
|
@@ -174,15 +199,17 @@ export async function createOidc<
|
|
|
174
199
|
"silent_redirect_uri": `${publicUrl}/silent-sso.html?${configHashKey}=${configHash}`
|
|
175
200
|
});
|
|
176
201
|
|
|
202
|
+
// NOTE: Only useful for Firefox, see note bellow.
|
|
177
203
|
let lastPublicRoute: string | undefined = undefined;
|
|
178
204
|
|
|
179
|
-
|
|
205
|
+
// NOTE: To call only if not logged in.
|
|
206
|
+
const startTrackingLastPublicRoute = () => {
|
|
180
207
|
const realPushState = history.pushState.bind(history);
|
|
181
208
|
history.pushState = function pushState(...args) {
|
|
182
209
|
lastPublicRoute = window.location.href;
|
|
183
210
|
return realPushState(...args);
|
|
184
211
|
};
|
|
185
|
-
}
|
|
212
|
+
};
|
|
186
213
|
|
|
187
214
|
let hasLoginBeenCalled = false;
|
|
188
215
|
|
|
@@ -243,6 +270,9 @@ export async function createOidc<
|
|
|
243
270
|
});
|
|
244
271
|
|
|
245
272
|
if (doesCurrentHrefRequiresAuth) {
|
|
273
|
+
// NOTE: In Firefox, when the user navigate back from the login page
|
|
274
|
+
// the state of the app is restored. We don't want that to happen,
|
|
275
|
+
// we want to redirect the user to the last public page.
|
|
246
276
|
const callback = () => {
|
|
247
277
|
if (document.visibilityState === "visible") {
|
|
248
278
|
document.removeEventListener("visibilitychange", callback);
|
|
@@ -259,6 +289,9 @@ export async function createOidc<
|
|
|
259
289
|
|
|
260
290
|
await oidcClientTsUserManager.signinRedirect({
|
|
261
291
|
redirect_uri,
|
|
292
|
+
// NOTE: This is for the behavior when the use presses the back button on the login pages.
|
|
293
|
+
// This is what happens when the user gave up the login process.
|
|
294
|
+
// We want to that to redirect to the last public page.
|
|
262
295
|
"redirectMethod": doesCurrentHrefRequiresAuth ? "replace" : "assign"
|
|
263
296
|
});
|
|
264
297
|
return new Promise<never>(() => {});
|
|
@@ -507,7 +540,9 @@ export async function createOidc<
|
|
|
507
540
|
"cause": error
|
|
508
541
|
});
|
|
509
542
|
|
|
510
|
-
console.error(`OIDC
|
|
543
|
+
console.error(`OIDC initialization error: ${initializationError.message}`);
|
|
544
|
+
|
|
545
|
+
startTrackingLastPublicRoute();
|
|
511
546
|
|
|
512
547
|
return id<Oidc.NotLoggedIn>({
|
|
513
548
|
...common,
|
|
@@ -521,6 +556,8 @@ export async function createOidc<
|
|
|
521
556
|
}
|
|
522
557
|
|
|
523
558
|
if (initialTokens === undefined) {
|
|
559
|
+
startTrackingLastPublicRoute();
|
|
560
|
+
|
|
524
561
|
return id<Oidc.NotLoggedIn>({
|
|
525
562
|
...common,
|
|
526
563
|
"isUserLoggedIn": false,
|
|
@@ -531,6 +568,10 @@ export async function createOidc<
|
|
|
531
568
|
|
|
532
569
|
let currentTokens = initialTokens;
|
|
533
570
|
|
|
571
|
+
const autoLogoutCountdownTickCallback = new Set<
|
|
572
|
+
(params: { secondsLeft: number | undefined }) => void
|
|
573
|
+
>();
|
|
574
|
+
|
|
534
575
|
const onTokenChanges = new Set<() => void>();
|
|
535
576
|
|
|
536
577
|
const oidc = id<Oidc.LoggedIn<DecodedIdToken>>({
|
|
@@ -583,24 +624,105 @@ export async function createOidc<
|
|
|
583
624
|
onTokenChanges.delete(onTokenChange);
|
|
584
625
|
}
|
|
585
626
|
};
|
|
627
|
+
},
|
|
628
|
+
"subscribeToAutoLogoutCountdown": tickCallback => {
|
|
629
|
+
autoLogoutCountdownTickCallback.add(tickCallback);
|
|
630
|
+
|
|
631
|
+
const unsubscribeFromAutoLogoutCountdown = () => {
|
|
632
|
+
autoLogoutCountdownTickCallback.delete(tickCallback);
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
return { unsubscribeFromAutoLogoutCountdown };
|
|
586
636
|
}
|
|
587
637
|
});
|
|
588
638
|
|
|
589
|
-
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
|
|
639
|
+
{
|
|
640
|
+
const getMsBeforeExpiration = () => {
|
|
641
|
+
// NOTE: In general the access token is supposed to have a shorter
|
|
642
|
+
// lifespan than the refresh token but we don't want to make any
|
|
643
|
+
// assumption here.
|
|
644
|
+
const tokenExpirationTime = Math.min(
|
|
645
|
+
currentTokens.accessTokenExpirationTime,
|
|
646
|
+
currentTokens.refreshTokenExpirationTime
|
|
647
|
+
);
|
|
593
648
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
649
|
+
return tokenExpirationTime - Date.now();
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
// NOTE: We refresh the token 25 seconds before it expires.
|
|
653
|
+
// If the token expiration time is less than 25 seconds we refresh the token when
|
|
654
|
+
// only 1/10 of the token time is left.
|
|
655
|
+
const renewMsBeforeExpires = Math.min(25_000, getMsBeforeExpiration() * 0.1);
|
|
656
|
+
|
|
657
|
+
(function scheduleRenew() {
|
|
658
|
+
const timer = setTimeout(async () => {
|
|
659
|
+
tokenChangeUnsubscribe();
|
|
660
|
+
|
|
661
|
+
try {
|
|
662
|
+
await oidc.renewTokens();
|
|
663
|
+
} catch {
|
|
664
|
+
// NOTE: Here semantically it's wrong. The user may very well be
|
|
665
|
+
// on a page that require auth.
|
|
666
|
+
// However there's no way to enforce the browser to redirect back to
|
|
667
|
+
// the last public route if the user press back on the login page.
|
|
668
|
+
// This is due to the fact that pushing to history only works if it's
|
|
669
|
+
// triggered by a user interaction.
|
|
670
|
+
await login({ "doesCurrentHrefRequiresAuth": false });
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
scheduleRenew();
|
|
674
|
+
}, getMsBeforeExpiration() - renewMsBeforeExpires);
|
|
675
|
+
|
|
676
|
+
const { unsubscribe: tokenChangeUnsubscribe } = oidc.subscribeToTokensChange(() => {
|
|
677
|
+
clearTimeout(timer);
|
|
678
|
+
tokenChangeUnsubscribe();
|
|
679
|
+
scheduleRenew();
|
|
680
|
+
});
|
|
681
|
+
})();
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
{
|
|
685
|
+
const { startCountdown } = createStartCountdown({
|
|
686
|
+
"getCountdownEndTime": () =>
|
|
687
|
+
__unsafe_ssoSessionIdleSeconds ?? currentTokens.refreshTokenExpirationTime,
|
|
688
|
+
"tickCallback": ({ secondsLeft }) => {
|
|
689
|
+
autoLogoutCountdownTickCallback.forEach(tickCallback => tickCallback({ secondsLeft }));
|
|
690
|
+
|
|
691
|
+
if (secondsLeft === 0) {
|
|
692
|
+
oidc.logout({ "redirectTo": "current page" });
|
|
693
|
+
}
|
|
599
694
|
}
|
|
695
|
+
});
|
|
600
696
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
697
|
+
let stopCountdown: (() => void) | undefined = undefined;
|
|
698
|
+
|
|
699
|
+
if ($isUserActive === undefined) {
|
|
700
|
+
$isUserActive = createIsUserActive({
|
|
701
|
+
"theUserIsConsideredInactiveAfterMsOfInactivity": 5_000
|
|
702
|
+
}).$isUserActive;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const { unsubscribe: unsubscribeFrom$isUserActive } = $isUserActive.subscribe(isUserActive => {
|
|
706
|
+
if (isUserActive) {
|
|
707
|
+
if (stopCountdown !== undefined) {
|
|
708
|
+
stopCountdown();
|
|
709
|
+
stopCountdown = undefined;
|
|
710
|
+
}
|
|
711
|
+
} else {
|
|
712
|
+
assert(stopCountdown === undefined);
|
|
713
|
+
stopCountdown = startCountdown().stopCountdown;
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
{
|
|
718
|
+
const hotReloadCleanupsForThisConfig = hotReloadCleanups.get(configHash);
|
|
719
|
+
assert(hotReloadCleanupsForThisConfig !== undefined);
|
|
720
|
+
hotReloadCleanupsForThisConfig.add(() => {
|
|
721
|
+
unsubscribeFrom$isUserActive();
|
|
722
|
+
stopCountdown?.();
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
}
|
|
604
726
|
|
|
605
727
|
return oidc;
|
|
606
728
|
}
|
package/src/react.tsx
CHANGED
|
@@ -14,9 +14,11 @@ export namespace OidcReact {
|
|
|
14
14
|
export type NotLoggedIn = Common & {
|
|
15
15
|
isUserLoggedIn: false;
|
|
16
16
|
login: Oidc.NotLoggedIn["login"];
|
|
17
|
+
initializationError: OidcInitializationError | undefined;
|
|
18
|
+
|
|
17
19
|
oidcTokens?: never;
|
|
18
20
|
logout?: never;
|
|
19
|
-
|
|
21
|
+
subscribeToAutoLogoutCountdown?: never;
|
|
20
22
|
};
|
|
21
23
|
|
|
22
24
|
export type LoggedIn<DecodedIdToken extends Record<string, unknown>> = Common & {
|
|
@@ -24,6 +26,10 @@ export namespace OidcReact {
|
|
|
24
26
|
oidcTokens: Oidc.Tokens<DecodedIdToken>;
|
|
25
27
|
logout: Oidc.LoggedIn["logout"];
|
|
26
28
|
renewTokens: Oidc.LoggedIn["renewTokens"];
|
|
29
|
+
subscribeToAutoLogoutCountdown: (
|
|
30
|
+
tickCallback: (params: { secondsLeft: number | undefined }) => void
|
|
31
|
+
) => { unsubscribeFromAutoLogoutCountdown: () => void };
|
|
32
|
+
|
|
27
33
|
login?: never;
|
|
28
34
|
initializationError?: never;
|
|
29
35
|
};
|
|
@@ -160,7 +166,8 @@ export function createReactOidc<
|
|
|
160
166
|
"isUserLoggedIn": true,
|
|
161
167
|
oidcTokens,
|
|
162
168
|
"logout": oidc.logout,
|
|
163
|
-
"renewTokens": oidc.renewTokens
|
|
169
|
+
"renewTokens": oidc.renewTokens,
|
|
170
|
+
"subscribeToAutoLogoutCountdown": oidc.subscribeToAutoLogoutCountdown
|
|
164
171
|
})
|
|
165
172
|
)
|
|
166
173
|
: id<OidcReact.NotLoggedIn>({
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export type StatefulObservable<T> = {
|
|
2
|
+
current: T;
|
|
3
|
+
subscribe: (next: (data: T) => void) => Subscription;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type StatefulReadonlyObservable<T> = {
|
|
7
|
+
readonly current: T;
|
|
8
|
+
subscribe: (next: (data: T) => void) => Subscription;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type Subscription = {
|
|
12
|
+
unsubscribe(): void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function createStatefulObservable<T>(getInitialValue: () => T): StatefulObservable<T> {
|
|
16
|
+
let nextFunctions: ((data: T) => void)[] = [];
|
|
17
|
+
|
|
18
|
+
const { get, set } = (() => {
|
|
19
|
+
let wrappedState: [T] | undefined = undefined;
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
"get": () => {
|
|
23
|
+
if (wrappedState === undefined) {
|
|
24
|
+
wrappedState = [getInitialValue()];
|
|
25
|
+
}
|
|
26
|
+
return wrappedState[0];
|
|
27
|
+
},
|
|
28
|
+
"set": (data: T) => {
|
|
29
|
+
wrappedState = [data];
|
|
30
|
+
|
|
31
|
+
nextFunctions.forEach(next => next(data));
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
|
|
36
|
+
return Object.defineProperty(
|
|
37
|
+
{
|
|
38
|
+
"current": null as any as T,
|
|
39
|
+
"subscribe": (next: (data: T) => void) => {
|
|
40
|
+
nextFunctions.push(next);
|
|
41
|
+
|
|
42
|
+
return { "unsubscribe": () => nextFunctions.splice(nextFunctions.indexOf(next), 1) };
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"current",
|
|
46
|
+
{
|
|
47
|
+
"enumerable": true,
|
|
48
|
+
get,
|
|
49
|
+
set
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createStatefulObservable } from "./StatefulObservable";
|
|
2
|
+
import { subscribeToUserInteraction } from "./subscribeToUserInteraction";
|
|
3
|
+
import { assert } from "tsafe/assert";
|
|
4
|
+
|
|
5
|
+
export function createIsUserActive(params: { theUserIsConsideredInactiveAfterMsOfInactivity: number }) {
|
|
6
|
+
const { theUserIsConsideredInactiveAfterMsOfInactivity } = params;
|
|
7
|
+
|
|
8
|
+
// this should set itself to false whenever the user had performed no user interaction for a certain amount of time
|
|
9
|
+
const $isUserActive = createStatefulObservable(() => true);
|
|
10
|
+
|
|
11
|
+
const scheduleSetInactive = () => {
|
|
12
|
+
const timer = setTimeout(() => {
|
|
13
|
+
assert($isUserActive.current);
|
|
14
|
+
$isUserActive.current = false;
|
|
15
|
+
}, theUserIsConsideredInactiveAfterMsOfInactivity);
|
|
16
|
+
return () => {
|
|
17
|
+
clearTimeout(timer);
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
let clearScheduledSetInactive = scheduleSetInactive();
|
|
22
|
+
|
|
23
|
+
const { unsubscribeFromUserInteraction } = subscribeToUserInteraction({
|
|
24
|
+
"throttleMs": 1_000,
|
|
25
|
+
"callback": () => {
|
|
26
|
+
clearScheduledSetInactive();
|
|
27
|
+
clearScheduledSetInactive = scheduleSetInactive();
|
|
28
|
+
|
|
29
|
+
if (!$isUserActive.current) {
|
|
30
|
+
$isUserActive.current = true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const disable$isUserActive = () => {
|
|
36
|
+
unsubscribeFromUserInteraction();
|
|
37
|
+
clearScheduledSetInactive();
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return { $isUserActive, disable$isUserActive };
|
|
41
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Deferred } from "./Deferred";
|
|
2
|
+
|
|
3
|
+
export function getPrUserInteraction() {
|
|
4
|
+
const d = new Deferred<void>();
|
|
5
|
+
|
|
6
|
+
const callback = () => {
|
|
7
|
+
d.resolve();
|
|
8
|
+
cleanup();
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const cleanup = () => {
|
|
12
|
+
window.document.removeEventListener("mousemove", callback, false);
|
|
13
|
+
window.document.removeEventListener("keydown", callback, false);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
window.document.addEventListener("mousemove", callback, false);
|
|
17
|
+
window.document.addEventListener("keydown", callback, false);
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
"prUserInteraction": d.pr,
|
|
21
|
+
"cancelPrUserInteraction": cleanup
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function createStartCountdown(params: {
|
|
2
|
+
tickCallback: (params: { secondsLeft: number | undefined }) => void;
|
|
3
|
+
getCountdownEndTime: () => number;
|
|
4
|
+
}) {
|
|
5
|
+
const { getCountdownEndTime, tickCallback } = params;
|
|
6
|
+
|
|
7
|
+
const getCountdownEndInMs = () => getCountdownEndTime() - Date.now();
|
|
8
|
+
|
|
9
|
+
function startCountdown() {
|
|
10
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
11
|
+
|
|
12
|
+
(async () => {
|
|
13
|
+
let secondsLeft = Math.floor(getCountdownEndInMs() / 1000);
|
|
14
|
+
|
|
15
|
+
while (secondsLeft >= 0) {
|
|
16
|
+
tickCallback({ secondsLeft });
|
|
17
|
+
|
|
18
|
+
await new Promise<void>(resolve => {
|
|
19
|
+
timer = setTimeout(resolve, 1_000);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
secondsLeft--;
|
|
23
|
+
}
|
|
24
|
+
})();
|
|
25
|
+
|
|
26
|
+
const stopCountdown = () => {
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
tickCallback({ "secondsLeft": undefined });
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return { stopCountdown };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { startCountdown };
|
|
35
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getPrUserInteraction } from "./getPrUserInteraction";
|
|
2
|
+
|
|
3
|
+
export function subscribeToUserInteraction(params: { throttleMs: number; callback: () => void }) {
|
|
4
|
+
const { throttleMs } = params;
|
|
5
|
+
|
|
6
|
+
const cleanups = new Set<() => void>();
|
|
7
|
+
|
|
8
|
+
(async function callee() {
|
|
9
|
+
const { cancelPrUserInteraction, prUserInteraction } = getPrUserInteraction();
|
|
10
|
+
|
|
11
|
+
cleanups.add(cancelPrUserInteraction);
|
|
12
|
+
|
|
13
|
+
await prUserInteraction;
|
|
14
|
+
|
|
15
|
+
params.callback();
|
|
16
|
+
|
|
17
|
+
await new Promise<void>(resolve => {
|
|
18
|
+
const timer = setTimeout(resolve, throttleMs);
|
|
19
|
+
cleanups.add(() => {
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
cleanups.clear();
|
|
25
|
+
callee();
|
|
26
|
+
})();
|
|
27
|
+
|
|
28
|
+
const unsubscribeFromUserInteraction = () => {
|
|
29
|
+
cleanups.forEach(cleanup => cleanup());
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return { unsubscribeFromUserInteraction };
|
|
33
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type StatefulObservable<T> = {
|
|
2
|
+
current: T;
|
|
3
|
+
subscribe: (next: (data: T) => void) => Subscription;
|
|
4
|
+
};
|
|
5
|
+
export type StatefulReadonlyObservable<T> = {
|
|
6
|
+
readonly current: T;
|
|
7
|
+
subscribe: (next: (data: T) => void) => Subscription;
|
|
8
|
+
};
|
|
9
|
+
export type Subscription = {
|
|
10
|
+
unsubscribe(): void;
|
|
11
|
+
};
|
|
12
|
+
export declare function createStatefulObservable<T>(getInitialValue: () => T): StatefulObservable<T>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createStatefulObservable = void 0;
|
|
4
|
+
function createStatefulObservable(getInitialValue) {
|
|
5
|
+
var nextFunctions = [];
|
|
6
|
+
var _a = (function () {
|
|
7
|
+
var wrappedState = undefined;
|
|
8
|
+
return {
|
|
9
|
+
"get": function () {
|
|
10
|
+
if (wrappedState === undefined) {
|
|
11
|
+
wrappedState = [getInitialValue()];
|
|
12
|
+
}
|
|
13
|
+
return wrappedState[0];
|
|
14
|
+
},
|
|
15
|
+
"set": function (data) {
|
|
16
|
+
wrappedState = [data];
|
|
17
|
+
nextFunctions.forEach(function (next) { return next(data); });
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
})(), get = _a.get, set = _a.set;
|
|
21
|
+
return Object.defineProperty({
|
|
22
|
+
"current": null,
|
|
23
|
+
"subscribe": function (next) {
|
|
24
|
+
nextFunctions.push(next);
|
|
25
|
+
return { "unsubscribe": function () { return nextFunctions.splice(nextFunctions.indexOf(next), 1); } };
|
|
26
|
+
}
|
|
27
|
+
}, "current", {
|
|
28
|
+
"enumerable": true,
|
|
29
|
+
get: get,
|
|
30
|
+
set: set
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
exports.createStatefulObservable = createStatefulObservable;
|
|
34
|
+
//# sourceMappingURL=StatefulObservable.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StatefulObservable.js","sourceRoot":"","sources":["../src/tools/StatefulObservable.ts"],"names":[],"mappings":";;;AAcA,SAAgB,wBAAwB,CAAI,eAAwB;IAChE,IAAI,aAAa,GAA0B,EAAE,CAAC;IAExC,IAAA,KAAe,CAAC;QAClB,IAAI,YAAY,GAAoB,SAAS,CAAC;QAE9C,OAAO;YACH,KAAK,EAAE;gBACH,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;oBAC7B,YAAY,GAAG,CAAC,eAAe,EAAE,CAAC,CAAC;gBACvC,CAAC;gBACD,OAAO,YAAY,CAAC,CAAC,CAAC,CAAC;YAC3B,CAAC;YACD,KAAK,EAAE,UAAC,IAAO;gBACX,YAAY,GAAG,CAAC,IAAI,CAAC,CAAC;gBAEtB,aAAa,CAAC,OAAO,CAAC,UAAA,IAAI,IAAI,OAAA,IAAI,CAAC,IAAI,CAAC,EAAV,CAAU,CAAC,CAAC;YAC9C,CAAC;SACJ,CAAC;IACN,CAAC,CAAC,EAAE,EAhBI,GAAG,SAAA,EAAE,GAAG,SAgBZ,CAAC;IAEL,OAAO,MAAM,CAAC,cAAc,CACxB;QACI,SAAS,EAAE,IAAgB;QAC3B,WAAW,EAAE,UAAC,IAAuB;YACjC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAEzB,OAAO,EAAE,aAAa,EAAE,cAAM,OAAA,aAAa,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EAApD,CAAoD,EAAE,CAAC;QACzF,CAAC;KACJ,EACD,SAAS,EACT;QACI,YAAY,EAAE,IAAI;QAClB,GAAG,KAAA;QACH,GAAG,KAAA;KACN,CACJ,CAAC;AACN,CAAC;AArCD,4DAqCC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createIsUserActive = void 0;
|
|
4
|
+
var StatefulObservable_1 = require("./StatefulObservable");
|
|
5
|
+
var subscribeToUserInteraction_1 = require("./subscribeToUserInteraction");
|
|
6
|
+
var assert_1 = require("tsafe/assert");
|
|
7
|
+
function createIsUserActive(params) {
|
|
8
|
+
var theUserIsConsideredInactiveAfterMsOfInactivity = params.theUserIsConsideredInactiveAfterMsOfInactivity;
|
|
9
|
+
// this should set itself to false whenever the user had performed no user interaction for a certain amount of time
|
|
10
|
+
var $isUserActive = (0, StatefulObservable_1.createStatefulObservable)(function () { return true; });
|
|
11
|
+
var scheduleSetInactive = function () {
|
|
12
|
+
var timer = setTimeout(function () {
|
|
13
|
+
(0, assert_1.assert)($isUserActive.current);
|
|
14
|
+
$isUserActive.current = false;
|
|
15
|
+
}, theUserIsConsideredInactiveAfterMsOfInactivity);
|
|
16
|
+
return function () {
|
|
17
|
+
clearTimeout(timer);
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
var clearScheduledSetInactive = scheduleSetInactive();
|
|
21
|
+
var unsubscribeFromUserInteraction = (0, subscribeToUserInteraction_1.subscribeToUserInteraction)({
|
|
22
|
+
"throttleMs": 1000,
|
|
23
|
+
"callback": function () {
|
|
24
|
+
clearScheduledSetInactive();
|
|
25
|
+
clearScheduledSetInactive = scheduleSetInactive();
|
|
26
|
+
if (!$isUserActive.current) {
|
|
27
|
+
$isUserActive.current = true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}).unsubscribeFromUserInteraction;
|
|
31
|
+
var disable$isUserActive = function () {
|
|
32
|
+
unsubscribeFromUserInteraction();
|
|
33
|
+
clearScheduledSetInactive();
|
|
34
|
+
};
|
|
35
|
+
return { $isUserActive: $isUserActive, disable$isUserActive: disable$isUserActive };
|
|
36
|
+
}
|
|
37
|
+
exports.createIsUserActive = createIsUserActive;
|
|
38
|
+
//# sourceMappingURL=createIsUserActive.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createIsUserActive.js","sourceRoot":"","sources":["../src/tools/createIsUserActive.ts"],"names":[],"mappings":";;;AAAA,2DAAgE;AAChE,2EAA0E;AAC1E,uCAAsC;AAEtC,SAAgB,kBAAkB,CAAC,MAAkE;IACzF,IAAA,8CAA8C,GAAK,MAAM,+CAAX,CAAY;IAElE,mHAAmH;IACnH,IAAM,aAAa,GAAG,IAAA,6CAAwB,EAAC,cAAM,OAAA,IAAI,EAAJ,CAAI,CAAC,CAAC;IAE3D,IAAM,mBAAmB,GAAG;QACxB,IAAM,KAAK,GAAG,UAAU,CAAC;YACrB,IAAA,eAAM,EAAC,aAAa,CAAC,OAAO,CAAC,CAAC;YAC9B,aAAa,CAAC,OAAO,GAAG,KAAK,CAAC;QAClC,CAAC,EAAE,8CAA8C,CAAC,CAAC;QACnD,OAAO;YACH,YAAY,CAAC,KAAK,CAAC,CAAC;QACxB,CAAC,CAAC;IACN,CAAC,CAAC;IAEF,IAAI,yBAAyB,GAAG,mBAAmB,EAAE,CAAC;IAE9C,IAAA,8BAA8B,GAAK,IAAA,uDAA0B,EAAC;QAClE,YAAY,EAAE,IAAK;QACnB,UAAU,EAAE;YACR,yBAAyB,EAAE,CAAC;YAC5B,yBAAyB,GAAG,mBAAmB,EAAE,CAAC;YAElD,IAAI,CAAC,aAAa,CAAC,OAAO,EAAE,CAAC;gBACzB,aAAa,CAAC,OAAO,GAAG,IAAI,CAAC;YACjC,CAAC;QACL,CAAC;KACJ,CAAC,+BAVoC,CAUnC;IAEH,IAAM,oBAAoB,GAAG;QACzB,8BAA8B,EAAE,CAAC;QACjC,yBAAyB,EAAE,CAAC;IAChC,CAAC,CAAC;IAEF,OAAO,EAAE,aAAa,eAAA,EAAE,oBAAoB,sBAAA,EAAE,CAAC;AACnD,CAAC;AApCD,gDAoCC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getPrUserInteraction = void 0;
|
|
4
|
+
var Deferred_1 = require("./Deferred");
|
|
5
|
+
function getPrUserInteraction() {
|
|
6
|
+
var d = new Deferred_1.Deferred();
|
|
7
|
+
var callback = function () {
|
|
8
|
+
d.resolve();
|
|
9
|
+
cleanup();
|
|
10
|
+
};
|
|
11
|
+
var cleanup = function () {
|
|
12
|
+
window.document.removeEventListener("mousemove", callback, false);
|
|
13
|
+
window.document.removeEventListener("keydown", callback, false);
|
|
14
|
+
};
|
|
15
|
+
window.document.addEventListener("mousemove", callback, false);
|
|
16
|
+
window.document.addEventListener("keydown", callback, false);
|
|
17
|
+
return {
|
|
18
|
+
"prUserInteraction": d.pr,
|
|
19
|
+
"cancelPrUserInteraction": cleanup
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
exports.getPrUserInteraction = getPrUserInteraction;
|
|
23
|
+
//# sourceMappingURL=getPrUserInteraction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getPrUserInteraction.js","sourceRoot":"","sources":["../src/tools/getPrUserInteraction.ts"],"names":[],"mappings":";;;AAAA,uCAAsC;AAEtC,SAAgB,oBAAoB;IAChC,IAAM,CAAC,GAAG,IAAI,mBAAQ,EAAQ,CAAC;IAE/B,IAAM,QAAQ,GAAG;QACb,CAAC,CAAC,OAAO,EAAE,CAAC;QACZ,OAAO,EAAE,CAAC;IACd,CAAC,CAAC;IAEF,IAAM,OAAO,GAAG;QACZ,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAC,WAAW,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;QAClE,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAC,SAAS,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;IACpE,CAAC,CAAC;IAEF,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAC,WAAW,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;IAC/D,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAC,SAAS,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;IAE7D,OAAO;QACH,mBAAmB,EAAE,CAAC,CAAC,EAAE;QACzB,yBAAyB,EAAE,OAAO;KACrC,CAAC;AACN,CAAC;AApBD,oDAoBC"}
|