4 Commits

Author SHA1 Message Date
cc6d541b4c feat: named contexts are ephemeral
All checks were successful
Release / release (push) Successful in 1m2s
Signed-off-by: Leo Galambos <lg@hq.egothor.org>
2025-08-28 18:36:51 +02:00
90efd93b72 feat: removeContext implemented
All checks were successful
Release / release (push) Successful in 57s
Signed-off-by: Leo Galambos <lg@hq.egothor.org>
2025-08-12 23:06:19 +02:00
b002d47d82 Javadoc fixes
All checks were successful
Release / release (push) Successful in 44s
chore: finalizing javadoc footer and other params

Signed-off-by: Leo Galambos <lg@hq.egothor.org>
2025-08-04 19:04:54 +02:00
2cf6d206e8 fix: Readme improved, javadoc added 2025-08-02 01:31:24 +02:00
5 changed files with 191 additions and 27 deletions

View File

@@ -13,19 +13,6 @@
<attribute name="test" value="true"/> <attribute name="test" value="true"/>
</attributes> </attributes>
</classpathentry> </classpathentry>
<classpathentry kind="src" output="bin/main" path="src/main/resources">
<attributes>
<attribute name="gradle_scope" value="main"/>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="bin/test" path="src/test/resources">
<attributes>
<attribute name="gradle_scope" value="test"/>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-21/"/> <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-21/"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/> <classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="output" path="bin/default"/> <classpathentry kind="output" path="bin/default"/>

View File

@@ -42,7 +42,7 @@ jobs:
apt install -y rsync apt install -y rsync
- name: Build and publish to Gitea Maven and JavaDoc to the website - name: Build and publish to Gitea Maven and JavaDoc to the website
run: ./gradlew clean publish uploadJavadoc --no-daemon -PgiteaToken=${{ secrets.CI_PUBLISH_TOKEN }} -PjavadocUser=${{ vars.JAVADOC_USER }} -PjavadocHost=${{ vars.JAVADOC_HOST }} -PjavadocPath=${{ vars.JAVADOC_PATH }} -PjavadocKeyPath=~/.ssh/id_rsa --debug run: ./gradlew clean publish uploadJavadoc --no-daemon -PgiteaToken=${{ secrets.CI_PUBLISH_TOKEN }} -PjavadocUser=${{ vars.JAVADOC_USER }} -PjavadocHost=${{ vars.JAVADOC_HOST }} -PjavadocPath=${{ vars.JAVADOC_PATH }} -PjavadocKeyPath=~/.ssh/id_rsa
- name: Generate release notes - name: Generate release notes
id: notes id: notes

View File

@@ -12,3 +12,28 @@ values and react to their changes in a clean and decoupled way.
- Publish/subscribe event bus for listening to value changes - Publish/subscribe event bus for listening to value changes
- Simple API for easy integration into existing projects - Simple API for easy integration into existing projects
- Enables modular design by decoupling components through events - Enables modular design by decoupling components through events
## Getting Started
Add to your `build.gradle`:
```groovy
repositories {
maven {
name = "GiteaMaven"
url = uri("https://gitea.egothor.org/api/packages/Egothor/maven")
}
// Use Maven Central for resolving dependencies.
mavenCentral()
}
dependencies {
implementation 'org.egothor:conflux:x.x.x'
}
```
## Documentation
- [Conflux JavaDoc](https://www.egothor.org/javadoc/conflux/)
- [Conflux Wiki](https://gitea.egothor.org/Egothor/conflux/wiki)

View File

@@ -6,6 +6,10 @@ plugins {
id 'pmd' id 'pmd'
} }
import java.time.LocalDate
def currentYear=LocalDate.now().getYear()
group 'org.egothor' group 'org.egothor'
version gitVersion(prefix:'release@') version gitVersion(prefix:'release@')
@@ -39,18 +43,37 @@ java {
} }
javadoc { javadoc {
failOnError = false failOnError = false
}
options.addBooleanOption('html5', true)
options.tags('apiNote:a:API Note:')
options.tags('implSpec:a:Implementation Requirements:')
options.tags('implNote:a:Implementation Note:')
options.tags('param')
options.tags('return')
options.tags('throws')
options.tags('since')
options.tags('version')
options.tags('serialData')
options.tags('factory')
options.tags('see')
options.use = true
options.author = true
options.version = true
options.windowTitle = 'Conflux'
options.bottom = '<div style="text-align: right; padding: 5px;">Copyright &copy; ' + currentYear +
' Egothor - Version ' + version +
' - <a href="https://gitea.egothor.org/Egothor/conflux/raw/branch/main/LICENSE">License</a>' +
'</div>'
source = sourceSets.main.allJava}
tasks.named('test') { tasks.named('test') {
// Use JUnit Platform for unit tests. // Use JUnit Platform for unit tests.
useJUnitPlatform() useJUnitPlatform()
} }
tasks.withType(Javadoc).configureEach {
options.bottom = "Copyright &copy; 2025 Egothor"
}
task uploadJavadoc(type: Exec) { task uploadJavadoc(type: Exec) {
dependsOn javadoc dependsOn javadoc

View File

@@ -34,9 +34,12 @@
*/ */
package conflux; package conflux;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/** /**
* A central context manager for storing and retrieving key-value pairs in a * A central context manager for storing and retrieving key-value pairs in a
@@ -61,17 +64,79 @@ public enum Ctx implements CtxInterface {
*/ */
INSTANCE; INSTANCE;
/**
* A registry of named contexts, allowing for multiple isolated logical
* contexts.
*/
private final Map<String, CtxInterface> contexts = new ConcurrentHashMap<>();
/** /**
* The default context instance used by this enum singleton. All interface * The default context instance used by this enum singleton. All interface
* method calls delegate to this context. * method calls delegate to this context.
*/ */
private final CtxInterface defaultCtx = new CtxInstance(); private final CtxInterface defaultCtx = new CtxInstance();
/**
* A registry of named contexts, allowing for multiple isolated logical
* contexts.
*/
private final Map<String, NamedWeakRef> contexts = new ConcurrentHashMap<>();
/**
* A reference queue to learn when weakly referenced contexts are collected.
*/
private final transient ReferenceQueue<CtxInterface> refQueue = new ReferenceQueue<>();
/**
* A weak reference wrapper that associates a {@link CtxInterface} instance (the
* referent) with its corresponding context name.
* <p>
* Instances of this class are enqueued into a {@link ReferenceQueue} once their
* referent becomes unreachable. The extra {@code name} field allows the owning
* registry to efficiently remove the corresponding entry from the context map
* without requiring a reverse lookup.
* </p>
*
* <h2>Usage</h2>
* <ul>
* <li>Created whenever a new named context is registered.</li>
* <li>Placed into a {@link ConcurrentHashMap} keyed by the same
* {@code name}.</li>
* <li>When the referent is garbage-collected, this reference is automatically
* enqueued, and the registry removes the map entry associated with
* {@link #name}.</li>
* </ul>
*
* <p>
* This design ensures that contexts are automatically deregistered when no
* strong references to them remain, preventing memory leaks while still
* allowing explicit removal via {@code removeContext}.
* </p>
*/
private static final class NamedWeakRef extends WeakReference<CtxInterface> {
/**
* The context name associated with the referent.
*/
private final String name;
/**
* Creates a new weak reference to the given context instance and associates it
* with the provided name.
*
* @param name the context name; used as the key in the registry
* @param referent the context instance being weakly referenced
* @param q the reference queue with which the weak reference is
* registered
*/
private NamedWeakRef(String name, CtxInterface referent, ReferenceQueue<CtxInterface> q) {
super(referent, q);
this.name = name;
}
}
/**
* Remove collected references from map.
*/
private void drainQueue() {
for (NamedWeakRef ref; (ref = (NamedWeakRef) refQueue.poll()) != null;) { // NOPMD
contexts.remove(ref.name, ref);
}
}
/** /**
* Returns a named context. If the context does not exist, it is lazily created. * Returns a named context. If the context does not exist, it is lazily created.
* *
@@ -82,7 +147,69 @@ public enum Ctx implements CtxInterface {
* @return the associated context instance * @return the associated context instance
*/ */
public CtxInterface getContext(String name) { public CtxInterface getContext(String name) {
return contexts.computeIfAbsent(name, k -> new CtxInstance()); if (name == null || name.isBlank()) {
throw new IllegalArgumentException("name must not be null or blank");
}
drainQueue();
NamedWeakRef ref = contexts.compute(name, (k, existing) -> {
CtxInterface alive = existing == null ? null : existing.get();
if (alive != null) {
return existing;
}
CtxInterface created = new CtxInstance();
return new NamedWeakRef(k, created, refQueue);
});
CtxInterface ctx = ref.get();
if (ctx == null) {
CtxInterface created = new CtxInstance();
NamedWeakRef fresh = new NamedWeakRef(name, created, refQueue);
contexts.put(name, fresh);
ctx = created;
}
return ctx;
}
/**
* Removes a named context previously created via {@link #getContext(String)}.
* <p>
* The context is atomically removed from the internal registry and then
* {@linkplain CtxInterface#clear() cleared} to release stored values and
* listener references. This operation is thread-safe and idempotent: if the
* specified context does not exist, the method returns {@code false} and
* performs no action.
* </p>
* <p>
* Calling {@code getContext(name)} again after a successful removal will create
* a brand-new, empty context. The default/global context represented by
* {@link #INSTANCE} is not affected.
* </p>
*
* @param name the name of the context to remove; must not be {@code null} or
* blank
* @return {@code true} if a context with the given name existed and was
* removed; {@code false} otherwise
* @throws IllegalArgumentException if {@code name} is {@code null} or blank
* @implNote Uses {@link ConcurrentHashMap#remove(Object)} for atomic removal,
* then invokes {@link CtxInterface#clear()} on the removed instance.
* @since 2025.08
*/
public boolean removeContext(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("name must not be null or blank");
}
drainQueue();
NamedWeakRef ref = contexts.remove(name);
if (ref == null) {
return false; // NOPMD
}
CtxInterface ctx = ref.get();
if (ctx != null) {
try {
ctx.clear();
} catch (RuntimeException ignore) { // NOPMD
}
}
return true;
} }
/** /**
@@ -91,7 +218,9 @@ public enum Ctx implements CtxInterface {
* @return an immutable set of context names * @return an immutable set of context names
*/ */
public Set<String> contextNames() { public Set<String> contextNames() {
return Set.copyOf(contexts.keySet()); drainQueue();
return Set.copyOf(contexts.entrySet().stream().filter(e -> e.getValue().get() != null).map(Map.Entry::getKey)
.collect(Collectors.toSet()));
} }
/** /**