From dbbb29484a64bb24ad0749f8730905f176d9a7b8 Mon Sep 17 00:00:00 2001 From: Alex Hope-O'Connor Date: Fri, 4 Sep 2026 17:40:27 +1000 Subject: [PATCH] Release DFTE v1.2.0 --- CHANGELOG.md | 5 ++ README.md | 2 +- docs/ASYNC_WEB.md | 48 +++++++++++++ include/TemplateEngineAsyncWeb.h | 116 +++++++++++++++++++++++++++++++ library.json | 2 +- 5 files changed, 171 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ad51d9..dcca594 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 1.2.0 + +- Add a borrowed async-response helper for fixed, caller-owned response slots. It avoids a per-request `shared_ptr` control allocation while releasing the slot safely on completion or disconnect. +- Add callback-enabled safe shared response helpers so dynamic contexts can return a bounded owner permit at the same time as their shared state is released. + ## 1.1.0 - Replace layout-affecting compile-time storage overrides with per-context, diff --git a/README.md b/README.md index 6703256..18126be 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Build [Hello Placeholder](examples/HelloPlaceholder/) to see the rendered result ```ini lib_deps = - DeviceFrameworkTemplateEngine=https://github.com/alexhopeoconnor/DFTE.git#v1.1.0 + DeviceFrameworkTemplateEngine=https://github.com/alexhopeoconnor/DFTE.git#v1.2.0 ``` PlatformIO checks out the Git ref after `#`; GitHub Release assets are unrelated. DFTE supports ESP8266 and ESP32 Arduino projects. diff --git a/docs/ASYNC_WEB.md b/docs/ASYNC_WEB.md index 3619651..9822ce6 100644 --- a/docs/ASYNC_WEB.md +++ b/docs/ASYNC_WEB.md @@ -24,6 +24,54 @@ void sendTemplate(AsyncWebServerRequest* request, const char* root) { The response retains the context while it streams. Releasing the request-owned `shared_ptr` on disconnect prevents state from leaking into later requests. +## Bounded fixed slots + +Use `beginBorrowedChunkedResponse()` when the application already owns a small, fixed response pool. It does not allocate a `shared_ptr` control block for the request. The release callback must be idempotent because it can run once when rendering finishes and again if the connection later disconnects. + +```cpp +struct ResponseSlot { + bool busy = false; + TemplateContext context; +}; + +ResponseSlot slots[2]; + +void releaseSlot(ResponseSlot& slot) { + slot.context.reset(); + slot.busy = false; +} + +void sendBoundedTemplate(AsyncWebServerRequest* request, const char* root) { + ResponseSlot* slot = nullptr; + for (auto& candidate : slots) { + if (!candidate.busy) { + slot = &candidate; + break; + } + } + if (slot == nullptr) { + request->send(503, "text/plain", "Busy"); + return; + } + + slot->busy = true; + slot->context.setRegistry(registry.get()); + TemplateRenderer::initializeContext(slot->context, root); + request->send(TemplateEngineAsyncWeb::beginBorrowedChunkedResponse( + request, "text/html; charset=utf-8", slot, + [](ResponseSlot& state, uint8_t* out, size_t size, size_t) { + return TemplateEngineAsyncWeb::renderTemplateChunkWithRetries( + state.context, out, size, 128); + }, + [](const ResponseSlot& state) { + return TemplateEngineAsyncWeb::isTemplateTerminal(state.context); + }, + releaseSlot)); +} +``` + +Use this form only while the slot itself has static or otherwise guaranteed lifetime. Use the `shared_ptr` form above for a request-owned dynamic context. + Use [StreamingAsync](../examples/StreamingAsync/) for a complete SoftAP/captive-portal project. Back to [documentation](README.md) ยท [project overview](../README.md). diff --git a/include/TemplateEngineAsyncWeb.h b/include/TemplateEngineAsyncWeb.h index 37731ba..b1b5649 100644 --- a/include/TemplateEngineAsyncWeb.h +++ b/include/TemplateEngineAsyncWeb.h @@ -85,6 +85,103 @@ AsyncWebServerResponse* beginSafeChunkedResponse(AsyncWebServerRequest* request, }); } +// A bounded embedded server can own response state in a fixed slot instead of +// allocating a shared control block for every request. The caller guarantees +// that state remains valid until release() is called. release() must be safe +// to call more than once because a completed request can later disconnect. +template +AsyncWebServerResponse* beginBorrowedChunkedResponse(AsyncWebServerRequest* request, + const char* contentType, + StateT* state, + FillFn fill, + IsDoneFn isDone, + ReleaseFn release) { + request->onDisconnect([state, release]() mutable { + if (state != nullptr) { + release(*state); + } + }); + + return request->beginChunkedResponse(contentType, + [state, fill, isDone, release](uint8_t* buffer, size_t maxLen, size_t index) mutable -> size_t { + if (state == nullptr) { + return 0; + } + + if (isDone(*state)) { + release(*state); + return 0; + } + + if (maxLen == 0) { + return RESPONSE_TRY_AGAIN; + } + + size_t written = fill(*state, buffer, maxLen, index); + if (written == RESPONSE_TRY_AGAIN) { + return RESPONSE_TRY_AGAIN; + } + + if (written > 0) { + return written; + } + + if (isDone(*state)) { + release(*state); + return 0; + } + + yieldForChunkRetry(); + return RESPONSE_TRY_AGAIN; + }); +} + +// Variant for dynamically allocated state when the owner also needs an +// idempotent completion notification (for example, returning a bounded web +// response permit). The original overload intentionally remains available for +// callers that only need shared ownership. +template +AsyncWebServerResponse* beginSafeChunkedResponse(AsyncWebServerRequest* request, + const char* contentType, + const std::shared_ptr& sharedState, + FillFn fill, + IsDoneFn isDone, + ReleaseFn release) { + request->onDisconnect([state = sharedState, release]() mutable { + release(); + state.reset(); + }); + + return request->beginChunkedResponse(contentType, + [state = sharedState, fill, isDone, release](uint8_t* buffer, size_t maxLen, size_t index) mutable -> size_t { + if (!state) { + return 0; + } + + if (maxLen == 0) { + return RESPONSE_TRY_AGAIN; + } + + size_t written = fill(*state, buffer, maxLen, index); + if (written == RESPONSE_TRY_AGAIN) { + return RESPONSE_TRY_AGAIN; + } + + if (written > 0) { + return written; + } + + if (isDone(*state)) { + release(); + state.reset(); + return 0; + } + + yieldForChunkRetry(); + return RESPONSE_TRY_AGAIN; + }); +} + template AsyncWebServerResponse* beginSafeChunkedResponse(AsyncWebServerRequest* request, const String& contentType, @@ -128,6 +225,25 @@ AsyncWebServerResponse* beginSafeChunkedResponse(AsyncWebServerRequest* request, }); } +template +AsyncWebServerResponse* beginSafeTemplateResponse(AsyncWebServerRequest* request, + const char* contentType, + const std::shared_ptr& sharedContext, + unsigned maxNoProgressRetries, + ReleaseFn release) { + return beginSafeChunkedResponse( + request, + contentType, + sharedContext, + [maxNoProgressRetries](ContextT& context, uint8_t* buffer, size_t maxLen, size_t /*index*/) -> size_t { + return renderTemplateChunkWithRetries(context, buffer, maxLen, maxNoProgressRetries); + }, + [](const ContextT& context) -> bool { + return isTemplateTerminal(context); + }, + release); +} + template AsyncWebServerResponse* beginSafeTemplateResponse(AsyncWebServerRequest* request, const char* contentType, diff --git a/library.json b/library.json index 6872308..3dd8650 100644 --- a/library.json +++ b/library.json @@ -1,6 +1,6 @@ { "name": "DeviceFrameworkTemplateEngine", - "version": "1.1.0", + "version": "1.2.0", "description": "Memory-efficient streaming template engine for ESP8266/ESP32 with chunked rendering support. Designed for embedded web interfaces with PROGMEM template support.", "keywords": [ "template",