rename(phase 1): CapaKraken → Nexus across code, UI, docs, CI (#61)
CI / Architecture Guardrails (push) Successful in 2m38s
CI / Assistant Split Regression (push) Successful in 3m33s
CI / Typecheck (push) Successful in 3m51s
CI / Lint (push) Successful in 5m2s
CI / E2E Tests (push) Has been cancelled
CI / Fresh-Linux Docker Deploy (push) Has been cancelled
CI / Release Images (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Architecture Guardrails (push) Successful in 2m38s
CI / Assistant Split Regression (push) Successful in 3m33s
CI / Typecheck (push) Successful in 3m51s
CI / Lint (push) Successful in 5m2s
CI / E2E Tests (push) Has been cancelled
CI / Fresh-Linux Docker Deploy (push) Has been cancelled
CI / Release Images (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
rename(phase 1): CapaKraken → Nexus across code, UI, docs, CI (#61) Co-authored-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com> Co-committed-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com>
This commit was merged in pull request #61.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { Redis } from "ioredis";
|
||||
import { PermissionKey, SSE_EVENT_TYPES, type SseEventType } from "@capakraken/shared";
|
||||
import { PermissionKey, SSE_EVENT_TYPES, type SseEventType } from "@nexus/shared";
|
||||
import { logger } from "../lib/logger.js";
|
||||
|
||||
export type UserSseAudience = `user:${string}`;
|
||||
@@ -73,7 +73,11 @@ function assertValidSseAudience(audience: string): SseAudience {
|
||||
}
|
||||
|
||||
export function canonicalizeSseAudiences(audiences?: Iterable<SseAudience>): SseAudience[] {
|
||||
return [...new Set(Array.from(audiences ?? [], (audience) => assertValidSseAudience(audience)).filter(Boolean))].sort();
|
||||
return [
|
||||
...new Set(
|
||||
Array.from(audiences ?? [], (audience) => assertValidSseAudience(audience)).filter(Boolean),
|
||||
),
|
||||
].sort();
|
||||
}
|
||||
|
||||
function getBufferKey(type: SseEventType, audience: readonly SseAudience[]): string {
|
||||
@@ -139,7 +143,12 @@ function flushEventType(type: SseEventType, audience: readonly SseAudience[]): v
|
||||
|
||||
const event: SseEvent =
|
||||
entry.payloads.length === 1
|
||||
? { type, payload: entry.payloads[0]!, timestamp: entry.firstTimestamp, audience: entry.audience }
|
||||
? {
|
||||
type,
|
||||
payload: entry.payloads[0]!,
|
||||
timestamp: entry.firstTimestamp,
|
||||
audience: entry.audience,
|
||||
}
|
||||
: {
|
||||
type,
|
||||
payload: { _batch: entry.payloads },
|
||||
@@ -159,7 +168,12 @@ export function flushPendingEvents(): void {
|
||||
const [type] = key.split("::") as [SseEventType];
|
||||
const event: SseEvent =
|
||||
entry.payloads.length === 1
|
||||
? { type, payload: entry.payloads[0]!, timestamp: entry.firstTimestamp, audience: entry.audience }
|
||||
? {
|
||||
type,
|
||||
payload: entry.payloads[0]!,
|
||||
timestamp: entry.firstTimestamp,
|
||||
audience: entry.audience,
|
||||
}
|
||||
: {
|
||||
type,
|
||||
payload: { _batch: entry.payloads },
|
||||
@@ -180,11 +194,13 @@ export function cancelPendingEvents(): void {
|
||||
}
|
||||
|
||||
// Redis connection — use env var REDIS_URL or fallback to default dev URL
|
||||
const REDIS_URL = process.env["REDIS_URL"] ?? (
|
||||
process.env["NODE_ENV"] !== "production"
|
||||
const REDIS_URL =
|
||||
process.env["REDIS_URL"] ??
|
||||
(process.env["NODE_ENV"] !== "production"
|
||||
? (console.warn("[env] REDIS_URL not set, using localhost fallback"), "redis://localhost:6380")
|
||||
: (() => { throw new Error("REDIS_URL required in production"); })()
|
||||
);
|
||||
: (() => {
|
||||
throw new Error("REDIS_URL required in production");
|
||||
})());
|
||||
const CHANNEL = "capakraken:sse";
|
||||
|
||||
let publisher: Redis | null = null;
|
||||
@@ -194,7 +210,10 @@ function getPublisher(): Redis {
|
||||
if (!publisher) {
|
||||
publisher = new Redis(REDIS_URL, { lazyConnect: false, enableReadyCheck: false });
|
||||
publisher.on("error", (e: unknown) => {
|
||||
logger.warn({ err: e, redisUrl: REDIS_URL, channel: CHANNEL }, "Redis publisher emitted an error");
|
||||
logger.warn(
|
||||
{ err: e, redisUrl: REDIS_URL, channel: CHANNEL },
|
||||
"Redis publisher emitted an error",
|
||||
);
|
||||
});
|
||||
}
|
||||
return publisher;
|
||||
@@ -205,10 +224,16 @@ function setupSubscriber(): void {
|
||||
try {
|
||||
subscriber = new Redis(REDIS_URL, { lazyConnect: false, enableReadyCheck: false });
|
||||
subscriber.on("error", (e: unknown) => {
|
||||
logger.warn({ err: e, redisUrl: REDIS_URL, channel: CHANNEL }, "Redis subscriber emitted an error");
|
||||
logger.warn(
|
||||
{ err: e, redisUrl: REDIS_URL, channel: CHANNEL },
|
||||
"Redis subscriber emitted an error",
|
||||
);
|
||||
});
|
||||
void subscriber.subscribe(CHANNEL).catch((err: unknown) => {
|
||||
logger.warn({ err, redisUrl: REDIS_URL, channel: CHANNEL }, "Redis subscribe failed for SSE event bus");
|
||||
logger.warn(
|
||||
{ err, redisUrl: REDIS_URL, channel: CHANNEL },
|
||||
"Redis subscribe failed for SSE event bus",
|
||||
);
|
||||
});
|
||||
subscriber.on("message", (_channel: string, message: string) => {
|
||||
try {
|
||||
@@ -229,7 +254,10 @@ function setupSubscriber(): void {
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
logger.warn({ err: e, redisUrl: REDIS_URL, channel: CHANNEL }, "Redis unavailable, SSE event bus will run in local-only mode");
|
||||
logger.warn(
|
||||
{ err: e, redisUrl: REDIS_URL, channel: CHANNEL },
|
||||
"Redis unavailable, SSE event bus will run in local-only mode",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,26 +285,38 @@ class EventBus {
|
||||
// Broadcast via Redis (all instances receive via subscriber.on("message"))
|
||||
try {
|
||||
const pub = getPublisher();
|
||||
void pub.publish(
|
||||
CHANNEL,
|
||||
JSON.stringify({
|
||||
type: normalizedEvent.type,
|
||||
payload: normalizedEvent.payload,
|
||||
timestamp: normalizedEvent.timestamp,
|
||||
audience: normalizedEvent.audience,
|
||||
}),
|
||||
).catch((e: unknown) => {
|
||||
logger.warn({ err: e, redisUrl: REDIS_URL, channel: CHANNEL }, "Redis publish promise rejected, falling back to local-only SSE delivery");
|
||||
publishLocal(normalizedEvent);
|
||||
});
|
||||
void pub
|
||||
.publish(
|
||||
CHANNEL,
|
||||
JSON.stringify({
|
||||
type: normalizedEvent.type,
|
||||
payload: normalizedEvent.payload,
|
||||
timestamp: normalizedEvent.timestamp,
|
||||
audience: normalizedEvent.audience,
|
||||
}),
|
||||
)
|
||||
.catch((e: unknown) => {
|
||||
logger.warn(
|
||||
{ err: e, redisUrl: REDIS_URL, channel: CHANNEL },
|
||||
"Redis publish promise rejected, falling back to local-only SSE delivery",
|
||||
);
|
||||
publishLocal(normalizedEvent);
|
||||
});
|
||||
} catch (e) {
|
||||
logger.warn({ err: e, redisUrl: REDIS_URL, channel: CHANNEL }, "Redis publish failed, falling back to local-only SSE delivery");
|
||||
logger.warn(
|
||||
{ err: e, redisUrl: REDIS_URL, channel: CHANNEL },
|
||||
"Redis publish failed, falling back to local-only SSE delivery",
|
||||
);
|
||||
// Deliver locally when Redis is unavailable
|
||||
publishLocal(normalizedEvent);
|
||||
}
|
||||
}
|
||||
|
||||
emit(type: SseEventType, payload: Record<string, unknown>, audience: Iterable<SseAudience> = []): void {
|
||||
emit(
|
||||
type: SseEventType,
|
||||
payload: Record<string, unknown>,
|
||||
audience: Iterable<SseAudience> = [],
|
||||
): void {
|
||||
this.publish({
|
||||
type,
|
||||
payload,
|
||||
@@ -327,7 +367,11 @@ export const emitAllocationCreated = (allocation: Record<string, unknown>) =>
|
||||
export const emitAllocationUpdated = (allocation: Record<string, unknown>) =>
|
||||
eventBus.emit(SSE_EVENT_TYPES.ALLOCATION_UPDATED, allocation, buildPlanningAudiences(allocation));
|
||||
|
||||
export const emitAllocationDeleted = (allocationId: string, projectId: string, resourceId?: string | null) =>
|
||||
export const emitAllocationDeleted = (
|
||||
allocationId: string,
|
||||
projectId: string,
|
||||
resourceId?: string | null,
|
||||
) =>
|
||||
eventBus.emit(
|
||||
SSE_EVENT_TYPES.ALLOCATION_DELETED,
|
||||
{ allocationId, projectId, ...(resourceId ? { resourceId } : {}) },
|
||||
@@ -338,11 +382,9 @@ export const emitProjectShifted = (project: Record<string, unknown>) =>
|
||||
eventBus.emit(SSE_EVENT_TYPES.PROJECT_SHIFTED, project, buildPlanningAudiences(project));
|
||||
|
||||
export const emitBudgetWarning = (projectId: string, payload: Record<string, unknown>) =>
|
||||
eventBus.emit(
|
||||
SSE_EVENT_TYPES.BUDGET_WARNING,
|
||||
{ projectId, ...payload },
|
||||
[permissionAudience(PermissionKey.MANAGE_ALLOCATIONS)],
|
||||
);
|
||||
eventBus.emit(SSE_EVENT_TYPES.BUDGET_WARNING, { projectId, ...payload }, [
|
||||
permissionAudience(PermissionKey.MANAGE_ALLOCATIONS),
|
||||
]);
|
||||
|
||||
export const emitVacationCreated = (vacation: Record<string, unknown>) =>
|
||||
eventBus.emit(SSE_EVENT_TYPES.VACATION_CREATED, vacation, buildPlanningAudiences(vacation));
|
||||
@@ -358,16 +400,24 @@ export const emitVacationDeleted = (vacationId: string, resourceId: string) =>
|
||||
);
|
||||
|
||||
export const emitRoleCreated = (role: Record<string, unknown>) =>
|
||||
eventBus.emit(SSE_EVENT_TYPES.ROLE_CREATED, role, [permissionAudience(PermissionKey.MANAGE_ROLES)]);
|
||||
eventBus.emit(SSE_EVENT_TYPES.ROLE_CREATED, role, [
|
||||
permissionAudience(PermissionKey.MANAGE_ROLES),
|
||||
]);
|
||||
|
||||
export const emitRoleUpdated = (role: Record<string, unknown>) =>
|
||||
eventBus.emit(SSE_EVENT_TYPES.ROLE_UPDATED, role, [permissionAudience(PermissionKey.MANAGE_ROLES)]);
|
||||
eventBus.emit(SSE_EVENT_TYPES.ROLE_UPDATED, role, [
|
||||
permissionAudience(PermissionKey.MANAGE_ROLES),
|
||||
]);
|
||||
|
||||
export const emitRoleDeleted = (roleId: string) =>
|
||||
eventBus.emit(SSE_EVENT_TYPES.ROLE_DELETED, { roleId }, [permissionAudience(PermissionKey.MANAGE_ROLES)]);
|
||||
eventBus.emit(SSE_EVENT_TYPES.ROLE_DELETED, { roleId }, [
|
||||
permissionAudience(PermissionKey.MANAGE_ROLES),
|
||||
]);
|
||||
|
||||
export function emitNotificationCreated(userId: string, notificationId: string): void {
|
||||
eventBus.emit(SSE_EVENT_TYPES.NOTIFICATION_CREATED, { userId, notificationId }, [userAudience(userId)]);
|
||||
eventBus.emit(SSE_EVENT_TYPES.NOTIFICATION_CREATED, { userId, notificationId }, [
|
||||
userAudience(userId),
|
||||
]);
|
||||
}
|
||||
|
||||
export function emitTaskAssigned(userId: string, notificationId: string): void {
|
||||
@@ -379,7 +429,9 @@ export function emitTaskCompleted(userId: string, notificationId: string): void
|
||||
}
|
||||
|
||||
export function emitTaskStatusChanged(userId: string, notificationId: string): void {
|
||||
eventBus.emit(SSE_EVENT_TYPES.TASK_STATUS_CHANGED, { userId, notificationId }, [userAudience(userId)]);
|
||||
eventBus.emit(SSE_EVENT_TYPES.TASK_STATUS_CHANGED, { userId, notificationId }, [
|
||||
userAudience(userId),
|
||||
]);
|
||||
}
|
||||
|
||||
export function emitReminderDue(userId: string, notificationId: string): void {
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { resolvePermissions, type PermissionKey, type PermissionOverrides, SystemRole } from "@capakraken/shared";
|
||||
import {
|
||||
resolvePermissions,
|
||||
type PermissionKey,
|
||||
type PermissionOverrides,
|
||||
SystemRole,
|
||||
} from "@nexus/shared";
|
||||
import {
|
||||
canonicalizeSseAudiences,
|
||||
permissionAudience,
|
||||
|
||||
Reference in New Issue
Block a user