Gateway 화면을 route 단위로 지연 로딩하고 common runtime import를 실제 소유 leaf export로 분리한다.\n\npackage boundary와 route loading 회귀 테스트로 root barrel 재유입을 차단한다.
98 lines
3.4 KiB
TypeScript
98 lines
3.4 KiB
TypeScript
import {
|
|
createEmptyRealtimeReadModelInvalidation,
|
|
mergeRealtimeReadModelInvalidations,
|
|
resolveRealtimeReadModelInvalidation,
|
|
type RealtimeReadModelChanges,
|
|
type RealtimeReadModelInvalidation,
|
|
type RealtimeViewerIdentity,
|
|
} from '@sammo-ts/common/realtime/types';
|
|
|
|
export type DashboardReadModelIdentity = RealtimeViewerIdentity;
|
|
export type DashboardRefreshPlan = RealtimeReadModelInvalidation;
|
|
export type DashboardContextBundleInclude = {
|
|
context: boolean;
|
|
commandTable: boolean;
|
|
boardAccess: boolean;
|
|
};
|
|
|
|
export const resolveDashboardRefreshPlan = (
|
|
changes: RealtimeReadModelChanges,
|
|
identity: DashboardReadModelIdentity
|
|
): DashboardRefreshPlan => resolveRealtimeReadModelInvalidation(changes, identity);
|
|
|
|
export const resolveDashboardContextBundleInclude = (plan: DashboardRefreshPlan): DashboardContextBundleInclude => ({
|
|
context: plan.context,
|
|
commandTable: plan.commands,
|
|
boardAccess: plan.boardAccess,
|
|
});
|
|
|
|
type TimerHandle = ReturnType<typeof setTimeout>;
|
|
|
|
export interface MergedReadModelRefreshQueue {
|
|
request(invalidation: RealtimeReadModelInvalidation, refreshGrant: string): void;
|
|
cancelPending(): void;
|
|
}
|
|
|
|
export const createMergedReadModelRefreshQueue = (
|
|
refresh: (invalidation: RealtimeReadModelInvalidation, refreshGrant: string) => Promise<void>,
|
|
options: {
|
|
minIntervalMs?: number;
|
|
now?: () => number;
|
|
setTimer?: (callback: () => void, delayMs: number) => TimerHandle;
|
|
clearTimer?: (handle: TimerHandle) => void;
|
|
} = {}
|
|
): MergedReadModelRefreshQueue => {
|
|
const minIntervalMs = Math.max(0, options.minIntervalMs ?? 1_000);
|
|
const now = options.now ?? Date.now;
|
|
const setTimer = options.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs));
|
|
const clearTimer = options.clearTimer ?? ((handle) => clearTimeout(handle));
|
|
let pending = createEmptyRealtimeReadModelInvalidation();
|
|
let pendingRefreshGrant = '';
|
|
let hasPending = false;
|
|
let running = false;
|
|
let timer: TimerHandle | null = null;
|
|
let lastStartedAt = Number.NEGATIVE_INFINITY;
|
|
|
|
const schedule = () => {
|
|
if (!hasPending || running || timer !== null) {
|
|
return;
|
|
}
|
|
const delayMs = Math.max(0, lastStartedAt + minIntervalMs - now());
|
|
timer = setTimer(() => {
|
|
timer = null;
|
|
if (!hasPending || running) {
|
|
return;
|
|
}
|
|
const next = pending;
|
|
const nextRefreshGrant = pendingRefreshGrant;
|
|
pending = createEmptyRealtimeReadModelInvalidation();
|
|
pendingRefreshGrant = '';
|
|
hasPending = false;
|
|
running = true;
|
|
lastStartedAt = now();
|
|
void refresh(next, nextRefreshGrant).finally(() => {
|
|
running = false;
|
|
schedule();
|
|
});
|
|
}, delayMs);
|
|
};
|
|
|
|
return {
|
|
request: (invalidation, refreshGrant) => {
|
|
pending = hasPending ? mergeRealtimeReadModelInvalidations(pending, invalidation) : invalidation;
|
|
pendingRefreshGrant = refreshGrant;
|
|
hasPending = true;
|
|
schedule();
|
|
},
|
|
cancelPending: () => {
|
|
hasPending = false;
|
|
pending = createEmptyRealtimeReadModelInvalidation();
|
|
pendingRefreshGrant = '';
|
|
if (timer !== null) {
|
|
clearTimer(timer);
|
|
timer = null;
|
|
}
|
|
},
|
|
};
|
|
};
|