diff --git a/inngest-spring-boot-demo/src/main/java/com/inngest/springbootdemo/testfunctions/DebouncedFunction.java b/inngest-spring-boot-demo/src/main/java/com/inngest/springbootdemo/testfunctions/DebouncedFunction.java new file mode 100644 index 00000000..c58d3b12 --- /dev/null +++ b/inngest-spring-boot-demo/src/main/java/com/inngest/springbootdemo/testfunctions/DebouncedFunction.java @@ -0,0 +1,30 @@ +package com.inngest.springbootdemo.testfunctions; + +import com.inngest.FunctionContext; +import com.inngest.InngestFunction; +import com.inngest.InngestFunctionConfigBuilder; +import com.inngest.Step; +import org.jetbrains.annotations.NotNull; + +import java.time.Duration; + +public class DebouncedFunction extends InngestFunction { + + @NotNull + @Override + public InngestFunctionConfigBuilder config(InngestFunctionConfigBuilder builder) { + return builder + .id("DebouncedFunction") + .name("Debounced Function") + .triggerEvent("test/debounced_2_second") + .debounce(Duration.ofSeconds(2)); + } + + private static int answer = 42; + + @Override + public Integer execute(FunctionContext ctx, Step step) { + return step.run("result", () -> answer++, Integer.class); + } +} + diff --git a/inngest-spring-boot-demo/src/test/java/com/inngest/springbootdemo/DebouncedFunctionIntegrationTest.java b/inngest-spring-boot-demo/src/test/java/com/inngest/springbootdemo/DebouncedFunctionIntegrationTest.java new file mode 100644 index 00000000..affc5d69 --- /dev/null +++ b/inngest-spring-boot-demo/src/test/java/com/inngest/springbootdemo/DebouncedFunctionIntegrationTest.java @@ -0,0 +1,35 @@ +package com.inngest.springbootdemo; + +import com.inngest.Inngest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.springframework.beans.factory.annotation.Autowired; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@IntegrationTest +@Execution(ExecutionMode.CONCURRENT) +class DebouncedFunctionIntegrationTest { + @Autowired + private DevServerComponent devServer; + + @Autowired + private Inngest client; + + @Test + void testDebouncedFunctionExecutesTrailingEdge() throws Exception { + String firstEvent = InngestFunctionTestHelpers.sendEvent(client, "test/debounced_2_second").getIds()[0]; + String secondEvent = InngestFunctionTestHelpers.sendEvent(client, "test/debounced_2_second").getIds()[0]; + + Thread.sleep(4000); + + // With debouncing, the first event is skipped in favor of the second one because they were both sent within + // the debounce period of 2 seconds + assertEquals(0, devServer.runsByEvent(firstEvent).data.length); + RunEntry secondRun = devServer.runsByEvent(secondEvent).first(); + + assertEquals("Completed", secondRun.getStatus()); + assertEquals(42, secondRun.getOutput()); + } +} diff --git a/inngest-spring-boot-demo/src/test/java/com/inngest/springbootdemo/DemoTestConfiguration.java b/inngest-spring-boot-demo/src/test/java/com/inngest/springbootdemo/DemoTestConfiguration.java index 7ab1cdca..693a3b13 100644 --- a/inngest-spring-boot-demo/src/test/java/com/inngest/springbootdemo/DemoTestConfiguration.java +++ b/inngest-spring-boot-demo/src/test/java/com/inngest/springbootdemo/DemoTestConfiguration.java @@ -24,6 +24,7 @@ protected HashMap functions() { addInngestFunction(functions, new InvokeFailureFunction()); addInngestFunction(functions, new TryCatchRunFunction()); addInngestFunction(functions, new ThrottledFunction()); + addInngestFunction(functions, new DebouncedFunction()); return functions; } diff --git a/inngest/src/main/kotlin/com/inngest/Function.kt b/inngest/src/main/kotlin/com/inngest/Function.kt index c03679bb..db9b26bf 100644 --- a/inngest/src/main/kotlin/com/inngest/Function.kt +++ b/inngest/src/main/kotlin/com/inngest/Function.kt @@ -81,6 +81,8 @@ internal class InternalFunctionConfig @Json(serializeNull = false) val throttle: Throttle? = null, @Json(serializeNull = false) + val debounce: Debounce? = null, + @Json(serializeNull = false) val batchEvents: BatchEvents? = null, val steps: Map, ) diff --git a/inngest/src/main/kotlin/com/inngest/InngestFunctionConfigBuilder.kt b/inngest/src/main/kotlin/com/inngest/InngestFunctionConfigBuilder.kt index 291ec000..a8600b91 100644 --- a/inngest/src/main/kotlin/com/inngest/InngestFunctionConfigBuilder.kt +++ b/inngest/src/main/kotlin/com/inngest/InngestFunctionConfigBuilder.kt @@ -14,6 +14,7 @@ class InngestFunctionConfigBuilder { private var concurrency: MutableList? = null private var retries = 3 private var throttle: Throttle? = null + private var debounce: Debounce? = null private var batchEvents: BatchEvents? = null /** @@ -147,6 +148,30 @@ class InngestFunctionConfigBuilder { burst: Int? = null, ): InngestFunctionConfigBuilder = apply { this.throttle = Throttle(limit, period, key, burst) } + /** + * Debounce delays functions for the `period` specified. If an event is sent, + * the function will not run until at least `period` has elapsed. + * + * If any new events are received that match the same debounce `key`, the + * function is rescheduled for another `period` delay, and the triggering + * event is replaced with the latest event received. + * + * See the [Debounce documentation](https://innge.st/debounce) for more + * information. + * + * @param period The period of time to delay after receiving the last trigger to run the function. + * @param key An optional key to use for debouncing. + * @param timeout The maximum time that a debounce can be extended before running. + * If events are continually received within the given period, a function + * will always run after the given timeout period. + */ + @JvmOverloads + fun debounce( + period: Duration, + key: String? = null, + timeout: Duration? = null, + ): InngestFunctionConfigBuilder = apply { this.debounce = Debounce(period, key, timeout) } + private fun buildSteps(serveUrl: String): Map { val scheme = serveUrl.split("://")[0] return mapOf( @@ -179,6 +204,7 @@ class InngestFunctionConfigBuilder { triggers, concurrency, throttle, + debounce, batchEvents, steps = buildSteps(serverUrl), ) @@ -248,6 +274,18 @@ internal data class Throttle val burst: Int? = null, ) +internal data class Debounce + @JvmOverloads + constructor( + @KlaxonDuration + val period: Duration, + @Json(serializeNull = false) + val key: String? = null, + @Json(serializeNull = false) + @KlaxonDuration + val timeout: Duration? = null, + ) + internal data class BatchEvents @JvmOverloads constructor(