feat: Vite extension and Maven plugin; data pools close on stop #19

Merged
Relism merged 5 commits from feature/ext-vite/replace-web-bundler into master 2026-09-22 16:27:47 +00:00
7 changed files with 110 additions and 1 deletions
Showing only changes of commit d6c018242f - Show all commits
@@ -112,6 +112,8 @@ public abstract class Repository<T, ID> {
- `Tx` in the `FlashContext`
- `TxManager` in the `FlashContext`
- an annotation processor for `@Transactional`
- `TxManager.close()` as an `onClose` callback, so stopping the app releases the manager's
session factory and connection pool; give the manager a pool you want closed with the app
This makes the data layer composable with Flash's extension system without global state.
@@ -34,6 +34,7 @@ public final class DataExtension implements FlashExtension {
@Override
public void configure(FlashRegistrar<?> app, FlashContext ctx) {
ctx.onClose(txManager::close);
ctx.provide(Tx.class, tx);
ctx.provide(TxManager.class, txManager);
if (data != null) ctx.provide(Data.class, data);
@@ -1,7 +1,11 @@
package dev.relism.flash.ext.data.core;
public interface TxManager {
public interface TxManager extends AutoCloseable {
TxStatus begin(TxDefinition definition);
void commit(TxStatus status);
void rollback(TxStatus status);
/** Releases what this manager was built on, its connection pool included. {@link dev.relism.flash.ext.data.DataExtension} calls it when the app stops. */
@Override
void close();
}
@@ -3,6 +3,10 @@ package dev.relism.flash.ext.data.hibernate;
import dev.relism.flash.ext.data.core.*;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import javax.sql.DataSource;
import java.util.Objects;
@@ -16,6 +20,24 @@ public class HibernateTxManager implements TxManager {
this.sf = Objects.requireNonNull(sessionFactory);
}
/**
* Closes the session factory, then the data source it was given ({@code jakarta.persistence.nonJtaDataSource}
* or {@code hibernate.connection.datasource}): Hibernate stops a pool it built itself, never one handed to it.
*/
@Override
public void close() {
ConnectionProvider connections = sf.unwrap(SessionFactoryImplementor.class).getServiceRegistry().getService(ConnectionProvider.class);
DataSource ds = connections != null && connections.isUnwrappableAs(DataSource.class) ? connections.unwrap(DataSource.class) : null;
sf.close();
if (ds instanceof AutoCloseable closeable) {
try {
closeable.close();
} catch (Exception e) {
throw new IllegalStateException("Failed to close the data source", e);
}
}
}
@Override
public TxStatus begin(TxDefinition definition) {
return switch (definition.propagation()) {
@@ -0,0 +1,48 @@
package dev.relism.flash.ext.data.hibernate;
import org.hibernate.SessionFactory;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.cfg.AvailableSettings;
import org.junit.jupiter.api.Test;
import javax.sql.DataSource;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.logging.Logger;
import static org.junit.jupiter.api.Assertions.assertTrue;
class HibernateTxManagerCloseTest {
/** Stands in for a pool: closeable, and it records being closed. */
static final class Pool implements DataSource, AutoCloseable {
boolean closed;
@Override public Connection getConnection() throws SQLException { return DriverManager.getConnection("jdbc:h2:mem:tx-close;DB_CLOSE_DELAY=-1"); }
@Override public Connection getConnection(String user, String password) throws SQLException { return getConnection(); }
@Override public void close() { closed = true; }
@Override public <T> T unwrap(Class<T> type) { throw new UnsupportedOperationException(); }
@Override public boolean isWrapperFor(Class<?> type) { return false; }
@Override public PrintWriter getLogWriter() { return null; }
@Override public void setLogWriter(PrintWriter out) {}
@Override public void setLoginTimeout(int seconds) {}
@Override public int getLoginTimeout() { return 0; }
@Override public Logger getParentLogger() { throw new UnsupportedOperationException(); }
}
@Test
void closingTheManagerClosesTheDataSourceHibernateWasGiven() {
Pool pool = new Pool();
SessionFactory sf = new MetadataSources(new StandardServiceRegistryBuilder()
.applySetting(AvailableSettings.JAKARTA_NON_JTA_DATASOURCE, pool)
.applySetting(AvailableSettings.DIALECT, "org.hibernate.dialect.H2Dialect")
.build()).buildMetadata().buildSessionFactory();
new HibernateTxManager(sf).close();
assertTrue(sf.isClosed());
assertTrue(pool.closed);
}
}
@@ -17,6 +17,18 @@ public class JdbcTxManager implements TxManager {
this.ds = Objects.requireNonNull(ds);
}
/** Closes the data source when it is closeable, as a pool is; a plain {@code DataSource} holds nothing to release. */
@Override
public void close() {
if (ds instanceof AutoCloseable closeable) {
try {
closeable.close();
} catch (Exception e) {
throw new IllegalStateException("Failed to close the data source", e);
}
}
}
@Override
public TxStatus begin(TxDefinition definition) {
return switch (definition.propagation()) {
@@ -0,0 +1,20 @@
package dev.relism.flash.ext.data.jdbc;
import com.zaxxer.hikari.HikariDataSource;
import dev.relism.flash.ext.data.DataExtension;
import dev.relism.flash.extension.FlashApp;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
class JdbcTxManagerCloseTest {
/** A pool outlives its app unless someone closes it; the data extension does, once requests have drained. */
@Test
void stoppingTheAppClosesThePool() {
HikariDataSource pool = new HikariDataSource();
pool.setJdbcUrl(TestDataSource.URL);
FlashApp.create(0).install(new DataExtension(new JdbcTxManager(pool))).start().stop().join();
assertTrue(pool.isClosed());
}
}