Skip to content

Commit 7d98bb5

Browse files
avernetobruchez
authored andcommitted
For #5890: support for about to expire listeners
1 parent 45a156b commit 7d98bb5

File tree

8 files changed

+69
-44
lines changed

8 files changed

+69
-44
lines changed

xforms-client-server/src/main/scala/org/orbeon/xforms/rpc/definitions.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ case class WireAjaxEventWithoutTarget(
6464
) extends WireAjaxEvent
6565

6666
case class ConfigurationProperties(
67-
sessionHeartbeat : Boolean,
68-
sessionHeartbeatDelay : Long,
67+
sessionHeartbeatEnabled : Boolean,
68+
maxInactiveIntervalMillis : Long,
6969
revisitHandling : String,
7070
delayBeforeIncrementalRequest : Int,
7171
delayBeforeAjaxTimeout : Long,

xforms-runtime/shared/src/main/scala/org/orbeon/oxf/xforms/XFormsContainingDocumentSupport.scala

+6-6
Original file line numberDiff line numberDiff line change
@@ -822,12 +822,12 @@ trait ContainingDocumentClientState {
822822

823823
val jsonInitializationData =
824824
ScriptBuilder.buildJsonInitializationData(
825-
containingDocument = this,
826-
rewriteResource = response.rewriteResourceURL(_: String, UrlRewriteMode.AbsolutePathOrRelative),
827-
rewriteAction = response.rewriteActionURL,
828-
controlsToInitialize = controls.getCurrentControlTree.rootOpt map (ScriptBuilder.gatherJavaScriptInitializations(_, includeValue = true)) getOrElse Nil,
829-
versionedResources = URLRewriterUtils.isResourcesVersioned,
830-
heartbeatDelay = XFormsStateManager.getHeartbeatDelay(this, externalContext)
825+
containingDocument = this,
826+
rewriteResource = response.rewriteResourceURL(_: String, UrlRewriteMode.AbsolutePathOrRelative),
827+
rewriteAction = response.rewriteActionURL,
828+
controlsToInitialize = controls.getCurrentControlTree.rootOpt map (ScriptBuilder.gatherJavaScriptInitializations(_, includeValue = true)) getOrElse Nil,
829+
versionedResources = URLRewriterUtils.isResourcesVersioned,
830+
maxInactiveIntervalMillis = XFormsStateManager.getMaxInactiveIntervalMillis(this, externalContext)
831831
)
832832

833833
_initializationData = (initializationScripts, jsonInitializationData).some

xforms-runtime/shared/src/main/scala/org/orbeon/oxf/xforms/processor/ScriptBuilder.scala

+12-12
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,9 @@ object ScriptBuilder {
8787
}
8888

8989
private def findConfigurationProperties(
90-
containingDocument : XFormsContainingDocument,
91-
versionedResources : Boolean,
92-
heartbeatDelay : Long
90+
containingDocument : XFormsContainingDocument,
91+
versionedResources : Boolean,
92+
maxInactiveIntervalMillis : Long
9393
): ConfigurationProperties = {
9494

9595
val staticState = containingDocument.staticState
@@ -104,8 +104,8 @@ object ScriptBuilder {
104104
versionedResources flatOption CoreCrossPlatformSupport.getApplicationResourceVersion
105105

106106
ConfigurationProperties(
107-
sessionHeartbeat = staticState.staticBooleanProperty(SessionHeartbeatProperty), // static
108-
sessionHeartbeatDelay = heartbeatDelay, // dynamic
107+
sessionHeartbeatEnabled = staticState.staticBooleanProperty(SessionHeartbeatProperty), // static
108+
maxInactiveIntervalMillis = maxInactiveIntervalMillis, // dynamic
109109
revisitHandling = staticState.staticStringProperty(RevisitHandlingProperty), // static
110110
delayBeforeIncrementalRequest = staticState.staticIntProperty(DelayBeforeIncrementalRequestProperty), // static
111111
delayBeforeAjaxTimeout = getAjaxTimeout, // global
@@ -127,12 +127,12 @@ object ScriptBuilder {
127127
}
128128

129129
def buildJsonInitializationData(
130-
containingDocument : XFormsContainingDocument,
131-
rewriteResource : String => String,
132-
rewriteAction : String => String,
133-
controlsToInitialize : List[(String, Option[String])],
134-
versionedResources : Boolean,
135-
heartbeatDelay : Long
130+
containingDocument : XFormsContainingDocument,
131+
rewriteResource : String => String,
132+
rewriteAction : String => String,
133+
controlsToInitialize : List[(String, Option[String])],
134+
versionedResources : Boolean,
135+
maxInactiveIntervalMillis : Long
136136
): String = {
137137

138138
val currentTime = System.currentTimeMillis
@@ -204,7 +204,7 @@ object ScriptBuilder {
204204
formId = containingDocument.getNamespacedFormId
205205
),
206206
configuration =
207-
findConfigurationProperties(containingDocument, versionedResources, heartbeatDelay),
207+
findConfigurationProperties(containingDocument, versionedResources, maxInactiveIntervalMillis),
208208
).asJson.noSpaces
209209
}
210210

xforms-runtime/shared/src/main/scala/org/orbeon/oxf/xforms/processor/handlers/xhtml/XHTMLHeadHandler.scala

+6-6
Original file line numberDiff line numberDiff line change
@@ -143,12 +143,12 @@ class XHTMLHeadHandler(
143143
writeContent(
144144
ScriptBuilder.buildInitializationCall(
145145
jsonInitialization = buildJsonInitializationData(
146-
containingDocument = containingDocument,
147-
rewriteResource = externalContext.getResponse.rewriteResourceURL(_: String, UrlRewriteMode.AbsolutePathOrRelative),
148-
rewriteAction = externalContext.getResponse.rewriteActionURL,
149-
controlsToInitialize = containingDocument.controls.getCurrentControlTree.rootOpt map (gatherJavaScriptInitializations(_, includeValue = true)) getOrElse Nil,
150-
versionedResources = isVersionedResources,
151-
heartbeatDelay = XFormsStateManager.getHeartbeatDelay(containingDocument, handlerContext.externalContext)
146+
containingDocument = containingDocument,
147+
rewriteResource = externalContext.getResponse.rewriteResourceURL(_: String, UrlRewriteMode.AbsolutePathOrRelative),
148+
rewriteAction = externalContext.getResponse.rewriteActionURL,
149+
controlsToInitialize = containingDocument.controls.getCurrentControlTree.rootOpt map (gatherJavaScriptInitializations(_, includeValue = true)) getOrElse Nil,
150+
versionedResources = isVersionedResources,
151+
maxInactiveIntervalMillis = XFormsStateManager.getMaxInactiveIntervalMillis(containingDocument, handlerContext.externalContext)
152152
),
153153
contextPathOpt = externalContext.getRequest.getFirstParamAsString(Constants.EmbeddingContextParameter),
154154
namespaceOpt = externalContext.getRequest.getFirstParamAsString(Constants.EmbeddingNamespaceParameter)

xforms-runtime/shared/src/main/scala/org/orbeon/oxf/xforms/state/XFormsStateManagerTrait.scala

+3-4
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,11 @@ trait XFormsStateManagerTrait extends XFormsStateLifecycle {
102102
*
103103
* @return delay in ms, or -1 is not applicable
104104
*/
105-
def getHeartbeatDelay(containingDocument: XFormsContainingDocument, externalContext: ExternalContext): Long =
106-
if (containingDocument.staticState.isClientStateHandling || ! containingDocument.isSessionHeartbeat) {
105+
def getMaxInactiveIntervalMillis(containingDocument: XFormsContainingDocument, externalContext: ExternalContext): Long =
106+
if (containingDocument.staticState.isClientStateHandling) {
107107
-1L
108108
} else {
109-
// 80% of session expiration time, in ms
110-
externalContext.getRequest.getSession(ForceSessionCreation).getMaxInactiveInterval * 800
109+
externalContext.getRequest.getSession(ForceSessionCreation).getMaxInactiveInterval * 1000
111110
}
112111

113112
// Ideally we wouldn't want to force session creation, but it's hard to implement the more elaborate expiration

xforms-web/src/main/scala/org/orbeon/xforms/AjaxClient.scala

+8-7
Original file line numberDiff line numberDiff line change
@@ -289,16 +289,17 @@ object AjaxClient {
289289
ErrorPanel.showError(Page.getXFormsFormFromNamespacedIdOrThrow(formId), detailsString)
290290
}
291291

292+
def newestEventTime: Long = EventQueue.newestEventTime
293+
292294
// Sending a heartbeat event if no event has been sent to server in the last time interval
293295
// determined by the `session-heartbeat-delay` property.
294-
def sendHeartBeatIfNeeded(heartBeatDelay: Long): Unit =
295-
if ((System.currentTimeMillis() - EventQueue.newestEventTime) >= heartBeatDelay)
296-
AjaxClient.fireEvent(
297-
AjaxEvent(
298-
eventName = EventNames.XXFormsSessionHeartbeat,
299-
form = Support.getFirstForm
300-
)
296+
def sendHeartBeat(): Unit =
297+
AjaxClient.fireEvent(
298+
AjaxEvent(
299+
eventName = EventNames.XXFormsSessionHeartbeat,
300+
form = Support.getFirstForm
301301
)
302+
)
302303

303304
private object EventQueue extends AjaxEventQueue[AjaxEvent] {
304305

xforms-web/src/main/scala/org/orbeon/xforms/InitSupport.scala

+17-7
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import org.orbeon.web.DomEventNames
2727
import org.orbeon.wsrp.WSRPSupport
2828
import org.orbeon.xforms
2929
import org.orbeon.xforms.EventNames.{KeyModifiersPropertyName, KeyTextPropertyName}
30+
import org.orbeon.xforms.Page.AboutToExpire
3031
import org.orbeon.xforms.StateHandling.StateResult
3132
import org.orbeon.xforms.facade._
3233
import org.orbeon.xforms.rpc.Initializations
@@ -319,14 +320,23 @@ object InitSupport {
319320
// The heartbeat is per servlet session and we only need one. But see https://github.com/orbeon/orbeon-forms/issues/2014.
320321
def initializeHeartBeatIfNeeded(configuration: rpc.ConfigurationProperties): Unit =
321322
if (! heartBeatInitialized) {
322-
if (configuration.sessionHeartbeat) {
323+
if (configuration.sessionHeartbeatEnabled) {
323324
// Say session is 60 minutes: heartbeat must come after 48 minutes and we check every 4.8 minutes
324-
val heartBeatDelay = configuration.sessionHeartbeatDelay
325-
val heartBeatCheckDelay = heartBeatDelay / 10
326-
if (heartBeatCheckDelay > 0) {
327-
logger.debug(s"setting heartbeat check every $heartBeatCheckDelay ms")
328-
js.timers.setInterval(heartBeatCheckDelay) {
329-
AjaxClient.sendHeartBeatIfNeeded(heartBeatDelay)
325+
val reactAfterMillis = (configuration.maxInactiveIntervalMillis * 0.8).toLong
326+
val checkEveryMillis = reactAfterMillis / 10
327+
if (checkEveryMillis > 0) {
328+
logger.debug(s"checking if getting close to session expiration every $checkEveryMillis ms")
329+
js.timers.setInterval(checkEveryMillis) {
330+
val elapsedMillisSinceLastEvent = System.currentTimeMillis() - AjaxClient.newestEventTime
331+
if (elapsedMillisSinceLastEvent >= reactAfterMillis) {
332+
if (configuration.sessionHeartbeatEnabled)
333+
AjaxClient.sendHeartBeat()
334+
val approxSessionExpiredTimeMillis = AjaxClient.newestEventTime + configuration.maxInactiveIntervalMillis
335+
Page.fireSessionAboutToExpire(AboutToExpire(
336+
configuration.sessionHeartbeatEnabled,
337+
approxSessionExpiredTimeMillis
338+
))
339+
}
330340
}
331341
}
332342
}

xforms-web/src/main/scala/org/orbeon/xforms/Page.scala

+15
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@ package org.orbeon.xforms
1515

1616

1717
import org.orbeon.oxf.util.StringUtils._
18+
import org.orbeon.oxf.externalcontext.ExternalContext.Scope.Session
1819
import org.orbeon.xforms
1920
import org.orbeon.xforms.Constants.FormClass
2021
import org.scalajs.dom.html
2122

2223
import scala.scalajs.js
24+
import scala.collection.mutable
25+
import scala.scalajs.js.Date
2326
import scala.scalajs.js.annotation.{JSExport, JSExportTopLevel}
2427

2528

@@ -130,12 +133,24 @@ object Page {
130133
}
131134
}
132135

136+
type AboutToExpireListener = AboutToExpire => Unit
137+
case class AboutToExpire(
138+
sessionHeartbeatEnabled : Boolean,
139+
approxSessionExpiredTimeMillis : Long
140+
)
141+
142+
def addSessionAboutToExpireListener (listener: AboutToExpireListener): Unit = sessionAboutToExpireListeners += listener
143+
def removeSessionAboutToExpireListener(listener: AboutToExpireListener): Unit = sessionAboutToExpireListeners -= listener
144+
def fireSessionAboutToExpire(aboutToExpire: AboutToExpire) : Unit = sessionAboutToExpireListeners.foreach(_(aboutToExpire))
145+
133146
// NOTE: This mechanism is deprecated by XBL and only used for the native `Upload` as of 2019-05-13.
134147
def registerControlConstructor(controlConstructor: () => Upload, predicate: html.Element => Boolean): Unit =
135148
controlConstructors ::= ConstructorPredicate(controlConstructor, predicate)
136149

137150
private object Private {
138151

152+
val sessionAboutToExpireListeners: mutable.ListBuffer[AboutToExpireListener] = mutable.ListBuffer.empty
153+
139154
case class ConstructorPredicate(controlConstructor: () => Upload, predicate: html.Element => Boolean)
140155

141156
lazy val loadingIndicator = new LoadingIndicator

0 commit comments

Comments
 (0)