Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions bruno-collection/mob.timer.sh/Delete goal for room.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
meta {
name: Delete goal for room
type: http
seq: 4
}

delete {
url: {{baseUrl}}/{{roomId}}/goal
body: json
auth: none
}

body:json {
{
"user": "{{user}}"
}
}

vars:pre-request {
roomId: test123
user: TestUser
}
15 changes: 15 additions & 0 deletions bruno-collection/mob.timer.sh/Get goal for room.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
meta {
name: Get goal for room
type: http
seq: 5
}

get {
url: {{baseUrl}}/{{roomId}}/goal
body: none
auth: none
}

vars:pre-request {
roomId: test123
}
24 changes: 24 additions & 0 deletions bruno-collection/mob.timer.sh/Set goal for room.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
meta {
name: Set goal for room
type: http
seq: 3
}

put {
url: {{baseUrl}}/{{roomId}}/goal
body: json
auth: none
}

body:json {
{
"goal": "{{goal}}",
"user": "{{user}}"
}
}

vars:pre-request {
roomId: test123
user: TestUser
goal: This is an awesome goal!
}
58 changes: 51 additions & 7 deletions src/main/java/sh/mob/timer/web/Room.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,43 @@ final class Room {
public static final TimerRequest NULL_TIMER_REQUEST =
new TimerRequest(0L, null, null, null, null);
private final String name;

private final List<TimerRequest> timerRequests = new CopyOnWriteArrayList<>();
private final Sinks.Many<TimerRequest> sink =
private final Sinks.Many<TimerRequest> timerRequestSink =
Sinks.many().replay().latestOrDefault(NULL_TIMER_REQUEST);

private Goal currentGoal = Goal.NO_GOAL;
private final Sinks.Many<Goal> goalRequestSink =
Sinks.many().replay().latestOrDefault(Goal.NO_GOAL);
Comment thread
hollesse marked this conversation as resolved.

Room(String name) {
this.name = name;
}

public void add(Long timer, String user, Instant requested) {
public void addTimer(Long timer, String user, Instant requested) {
var nextUser = findNextUser(user);
var timerRequest = new TimerRequest(timer, requested, user, nextUser, TimerType.TIMER);
timerRequests.add(timerRequest);
sink.tryEmitNext(timerRequest);
timerRequestSink.tryEmitNext(timerRequest);
}

public void setGoal(String text, String user, Instant requested) {
Comment thread
hollesse marked this conversation as resolved.
var newGoal = new Goal(text, user, requested);
currentGoal = newGoal ;
goalRequestSink.tryEmitNext(newGoal);
}

public void deleteGoal(String user, Instant requested) {
if(currentGoal.goal() != null){
currentGoal = Goal.deleted(user, requested);
goalRequestSink.tryEmitNext(currentGoal);
log.info(
"Delete current goal by user {} for room {}",
user,
name);
} else {
log.info("Try to delete current goal by user {} for room {}, but there is no current goal.", user, name);
}
}

private String findNextUser(String user) {
Expand Down Expand Up @@ -70,11 +94,15 @@ public void addBreaktimer(Long breaktimer, String user) {
lastTimerRequest().map(TimerRequest::getNextUser).orElse(null),
TimerType.BREAKTIMER);
timerRequests.add(timerRequest);
sink.tryEmitNext(timerRequest);
timerRequestSink.tryEmitNext(timerRequest);
}

public Sinks.Many<TimerRequest> timerRequestSink() {
return timerRequestSink;
}

public Sinks.Many<TimerRequest> sink() {
return sink;
public Sinks.Many<Goal> goalRequestSink() {
return goalRequestSink;
}

Optional<TimerRequest> lastTimerRequest() {
Expand All @@ -89,7 +117,7 @@ public void removeOldTimerRequests() {
this.timerRequests.removeIf(
timerRequest -> now.minus(24, HOURS).isAfter(timerRequest.getRequested()));
if (timerRequests.isEmpty()) {
sink.tryEmitNext(NULL_TIMER_REQUEST);
timerRequestSink.tryEmitNext(NULL_TIMER_REQUEST);
log.info("Emptied room {}", name);
}
}
Expand All @@ -98,6 +126,14 @@ public String name() {
return name;
}

public Goal currentGoal() {
return currentGoal;
}

public boolean hasGoal() {
return currentGoal.goal != null;
}

public List<TimerRequest> historyWithoutLatest() {
if (timerRequests.isEmpty()) {
return List.of();
Expand All @@ -117,6 +153,14 @@ private static boolean isTimerActive(TimerRequest timerRequest, Instant now) {
&& timerRequest.getRequested().plus(timerRequest.getTimer(), MINUTES).isAfter(now);
}

public record Goal(String goal, String user, Instant requested){
public static final Goal NO_GOAL = new Goal(null, null, null);

public static Goal deleted(String user, Instant requested){
return new Goal(null, user, requested);
}
}

public static final class TimerRequest {

enum TimerType {
Expand Down
134 changes: 53 additions & 81 deletions src/main/java/sh/mob/timer/web/RoomApiController.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,17 @@
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import sh.mob.timer.web.Room.TimerRequest;

@RestController
@RequestMapping()
public class RoomApiController {

private static final String SMOKETEST_ROOM_NAME = "testroom-310a9c47-515c-4ad7-a229-ae8efbab7387";
private static final Logger log = LoggerFactory.getLogger(RoomApiController.class);
private final RoomRepository roomRepository;
private final Clock clock;
Expand All @@ -50,11 +44,17 @@ public Flux<ServerSentEvent<Object>> getEventStream(
var room = roomRepository.get(roomId);

var timerRequestFlux =
room.sink()
room.timerRequestSink()
.asFlux()
.map(
timerRequest ->
ServerSentEvent.builder().event("TIMER_REQUEST").data(timerRequest).build());
var goalRequestFlux =
room.goalRequestSink()
.asFlux()
.map(
goalRequest ->
ServerSentEvent.builder().event("GOAL_REQUEST").data(goalRequest).build());
var keepAliveFlux =
Flux.interval(Duration.ofSeconds(5L))
.map(
Expand All @@ -67,7 +67,7 @@ public Flux<ServerSentEvent<Object>> getEventStream(
Flux.just(room.historyWithoutLatest())
.map(list -> ServerSentEvent.builder().event("INITIAL_HISTORY").data(list).build());

return Flux.concat(initialHistory, keepAliveFlux.mergeWith(timerRequestFlux));
return Flux.concat(initialHistory, keepAliveFlux.mergeWith(timerRequestFlux).mergeWith(goalRequestFlux));
}

@PutMapping("/{roomId:[A-Za-z0-9-_]+}")
Expand All @@ -76,14 +76,14 @@ public void publishEvent(@PathVariable String roomId, @RequestBody PutTimerReque
var room = roomRepository.get(roomId);
if (timerRequest.timer() != null) {
long timer = truncateTooLongTimers(timerRequest.timer());
room.add(
room.addTimer(
timer, timerRequest.user(), Instant.now(clock));
log.info(
"Add timer {} by user {} for room {}",
timerRequest.timer,
timerRequest.user,
room.name());
incrementTimerStatsExceptForTestRoom(room, timer);
stats.incrementTimer(room.name(), timer);
} else if (timerRequest.breaktimer() != null) {
long breaktimer = truncateTooLongTimers(timerRequest.breaktimer());
room.addBreaktimer(breaktimer, timerRequest.user());
Expand All @@ -92,91 +92,63 @@ public void publishEvent(@PathVariable String roomId, @RequestBody PutTimerReque
timerRequest.breaktimer(),
timerRequest.user,
room.name());
incrementBreakTimerStatsExceptForTestRoom(room, breaktimer);
stats.incrementBreaktimer(room.name(), breaktimer);
} else {
log.warn("Could not understand PUT request for room {}", roomId);
}
}

private void incrementBreakTimerStatsExceptForTestRoom(Room room, long breaktimer) {
if (!Objects.equals(room.name(), SMOKETEST_ROOM_NAME)) {
stats.incrementBreaktimer(breaktimer);
@PutMapping("/{roomId:[A-Za-z0-9-_]+}/goal")
@ResponseStatus(HttpStatus.ACCEPTED)
public void putGoal(@PathVariable String roomId, @RequestBody PutGoalRequest goalRequest) {
var room = roomRepository.get(roomId);
if (goalRequest.goal() != null) {
String goal = truncateTooLongGoal(goalRequest.goal());
room.setGoal(
goal, goalRequest.user(), Instant.now(clock));
log.info(
"Add goal \"{}\" by user {} for room {}",
goalRequest.goal(),
goalRequest.user(),
room.name());
stats.incrementGoalCount(room.name());
} else {
log.warn("Could not understand PUT goal request for room {}", roomId);
}
}

private void incrementTimerStatsExceptForTestRoom(Room room, long timer) {
if (!Objects.equals(room.name(), SMOKETEST_ROOM_NAME)) {
stats.incrementTimer(timer);
@DeleteMapping("/{roomId:[A-Za-z0-9-_]+}/goal")
@ResponseStatus(HttpStatus.ACCEPTED)
public void deleteGoal(@PathVariable String roomId, @RequestBody DeleteGoalRequest deleteGoalRequest) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why a delete goal? Could be a put to empty goal

var room = roomRepository.get(roomId);
room.deleteGoal(deleteGoalRequest.user(), Instant.now(clock));
}

@GetMapping("/{roomId:[A-Za-z0-9-_]+}/goal")
@ResponseStatus(HttpStatus.NO_CONTENT)
public ResponseEntity<GoalResponse> getGoal(@PathVariable String roomId) {
var room = roomRepository.get(roomId);
if (!room.hasGoal()){
return ResponseEntity.noContent().build();
}
return ResponseEntity.ofNullable(GoalResponse.of(room.currentGoal()));
}

private static long truncateTooLongTimers(Long timer) {
return Math.min(60 * 24, Math.max(0, timer));
}

static final class PutTimerRequest {

private final Long timer;
private final Long breaktimer;
private final String user;

PutTimerRequest(Long timer, Long breaktimer, String user) {
this.timer = timer;
this.user = user;
this.breaktimer = breaktimer;
}

public Long timer() {
return timer;
}

public Long breaktimer() {
return breaktimer;
}

public String user() {
return user;
}

public Long getTimer() {
return timer;
}

public Long getBreaktimer() {
return breaktimer;
}

public String getUser() {
return user;
}

@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (obj == null || obj.getClass() != this.getClass()) return false;
var that = (PutTimerRequest) obj;
return Objects.equals(this.timer, that.timer)
&& Objects.equals(this.breaktimer, that.breaktimer)
&& Objects.equals(this.user, that.user);
}

@Override
public int hashCode() {
return Objects.hash(timer, breaktimer, user);
}
private static String truncateTooLongGoal(String goal) {
return goal.length() > 256 ? goal.substring(0,256-1-3) + "...": goal;
}

@Override
public String toString() {
return "PutTimerRequest["
+ "timer="
+ timer
+ ", "
+ "breaktimer="
+ breaktimer
+ ", "
+ "user="
+ user
+ ']';
public record GoalResponse(String goal){
public static GoalResponse of(Room.Goal goal){
return new GoalResponse(goal.goal());
}
}

public record PutGoalRequest(String goal, String user){}
public record DeleteGoalRequest(String user){}
public record PutTimerRequest(Long timer, Long breaktimer, String user){}
}
6 changes: 5 additions & 1 deletion src/main/java/sh/mob/timer/web/RoomRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ Room get(String room) {
});
}

void deleteAll(){
repository.clear();
}

@Scheduled(fixedRateString = "PT1M")
void cleanUpUnusedRooms() {
repository.forEach((key, room) -> room.removeOldTimerRequests());
Expand All @@ -51,7 +55,7 @@ public long count() {

public long countConnections() {
return repository.values().stream()
.mapToLong(room -> room.sink().currentSubscriberCount())
.mapToLong(room -> room.timerRequestSink().currentSubscriberCount())
.sum();
}

Expand Down
Loading