refactor: 리펙토링

This commit is contained in:
DongHyeonka
2026-08-01 19:39:59 +09:00
parent 9c959ea2a5
commit c6da03369c
171 changed files with 20329 additions and 782 deletions
@@ -21,9 +21,11 @@ import {
type AppFailure,
} from "../../../contracts/errors.ts";
import type { QueryInvalidationTopic } from "../../../contracts/query-invalidation.ts";
import type {
BoundMutation,
BoundQuery,
import {
admitQueryResult,
type BoundMutation,
type BoundQuery,
type MutationDuplicatePolicy,
} from "../../../contracts/server-state.ts";
import { runtimeIdentityToken } from "../../../contracts/query-keys.ts";
import { useQueryInvalidationCoordinator } from "./query-invalidation-provider.tsx";
@@ -64,6 +66,8 @@ export function useApplicationQuery<Value>(
const profile = "profile" in options ? options.profile : undefined;
const scope = "scope" in options ? options.scope : undefined;
const identity = "identity" in options ? options.identity : undefined;
const measureResult =
"measureResult" in options ? options.measureResult : undefined;
const queryDefinitionId =
"definitionId" in options ? options.definitionId : "APPLICATION_QUERY";
const [staleFailure, setStaleFailure] = useState(false);
@@ -105,22 +109,22 @@ export function useApplicationQuery<Value>(
);
}
if (result.ok) {
if (
profile &&
!isAdmissibleResult(
if (profile && measureResult) {
const admission = admitQueryResult(
measureResult,
result.value,
profile.maxResultItems,
profile.maxEstimatedResultBytes,
)
) {
throw new ApplicationQueryError(
createFailure(
"RESULT_LIMIT_EXCEEDED",
"APPLICATION_QUERY",
0,
{ code: "RESULT_ADMISSION_LIMIT_EXCEEDED" },
),
profile,
);
if (!admission.ok) {
throw new ApplicationQueryError(
createFailure(
"RESULT_LIMIT_EXCEEDED",
queryDefinitionId,
0,
{ code: admission.code },
),
);
}
}
return result.value;
}
@@ -175,26 +179,34 @@ export function useApplicationQuery<Value>(
});
}
export function useApplicationMutation<Input, Value>(
options:
| BoundMutation<Input, Value>
| Readonly<{
execute(input: Input): Promise<ApplicationResult<Value>>;
invalidate?: readonly QueryInvalidationTopic[];
optimistic?: Readonly<{
queryKey: readonly unknown[];
update(previous: unknown, input: Input): unknown;
}>;
currentData?: unknown;
}>,
): Readonly<{
type LegacyMutationOptions<Input, Value> = Readonly<{
execute(input: Input): Promise<ApplicationResult<Value>>;
duplicatePolicy?: MutationDuplicatePolicy;
invalidate?: readonly QueryInvalidationTopic[];
optimistic?: Readonly<{
queryKey: readonly unknown[];
update(previous: unknown, input: Input): unknown;
}>;
currentData?: unknown;
}>;
type ApplicationMutationController<Input, Value> = Readonly<{
state: AsyncState;
submit(input: Input): Promise<ApplicationResult<Value>>;
resolveConflict(): Promise<void>;
}> {
}>;
export function useApplicationMutation<Input, Value>(
options: BoundMutation<Input, Value>,
): ApplicationMutationController<Input, Value>;
export function useApplicationMutation<Input, Value>(
options: LegacyMutationOptions<Input, Value>,
): ApplicationMutationController<Input, Value>;
export function useApplicationMutation<Input, Value>(
options: BoundMutation<Input, Value> | LegacyMutationOptions<Input, Value>,
): ApplicationMutationController<Input, Value> {
const queryClient = useQueryClient();
const invalidationCoordinator = useQueryInvalidationCoordinator();
const { execute } = options;
const invalidate = useMemo(
() => options.invalidate ?? [],
[options.invalidate],
@@ -204,7 +216,7 @@ export function useApplicationMutation<Input, Value>(
const definitionId =
"definitionId" in options ? options.definitionId : "LEGACY_MUTATION";
const duplicatePolicy =
"duplicatePolicy" in options ? options.duplicatePolicy : "JOIN_IDENTICAL";
"duplicatePolicy" in options ? options.duplicatePolicy : "REJECT_WHILE_ACTIVE";
const [conflict, setConflict] = useState<AppFailure | null>(null);
const scope = "scope" in options ? options.scope : undefined;
const mutation = useMutation<Value, ApplicationQueryError, Input>({
@@ -220,14 +232,21 @@ export function useApplicationMutation<Input, Value>(
),
);
}
const result = await execute(input);
const result =
"scope" in options
? await options.execute(input, { signal: options.scope.signal })
: await options.execute(input);
if (scope && !scope.isCurrent()) {
const effect =
!result.ok && result.error.effect !== undefined
? result.error.effect
: "MAYBE_APPLIED";
throw new ApplicationQueryError(
createFailure(
"SCOPE_GENERATION_CHANGED",
definitionId,
0,
{ code: "MUTATION_SCOPE_CHANGED" },
{ code: "MUTATION_SCOPE_CHANGED", effect },
),
);
}
@@ -276,7 +295,7 @@ export function useApplicationMutation<Input, Value>(
identityLease?.release();
return active;
}
if (active && duplicatePolicy === "REJECT_DUPLICATE") {
if (active && duplicatePolicy === "REJECT_WHILE_ACTIVE") {
identityLease?.release();
return Promise.resolve({
ok: false,
@@ -379,7 +398,7 @@ export function useApplicationMutation<Input, Value>(
mutationExecutions(queryClient).delete(identity);
}
});
if (duplicatePolicy !== "ALLOW_INDEPENDENT") {
if (duplicatePolicy !== "ALLOW_PARALLEL") {
mutationExecutions(queryClient).set(identity, pending);
}
return pending;
@@ -445,36 +464,3 @@ function optimisticLayers(
OPTIMISTIC_LAYER_RUNTIMES.set(queryClient, created);
return created;
}
function isAdmissibleResult(
value: unknown,
maxItems: number,
maxBytes: number,
): boolean {
try {
const seen = new WeakSet<object>();
let items = 0;
const visit = (candidate: unknown): boolean => {
if (candidate === null || ["string", "number", "boolean"].includes(typeof candidate)) {
return true;
}
if (!candidate || typeof candidate !== "object" || seen.has(candidate)) return false;
seen.add(candidate);
if (Array.isArray(candidate)) {
items += candidate.length;
return items <= maxItems && candidate.every(visit);
}
const prototype = Object.getPrototypeOf(candidate);
return (
(prototype === Object.prototype || prototype === null) &&
Object.values(candidate).every(visit)
);
};
return (
visit(value) &&
new TextEncoder().encode(JSON.stringify(value)).byteLength <= maxBytes
);
} catch {
return false;
}
}