108 lines
3.1 KiB
TypeScript
108 lines
3.1 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import type { ReactNode } from "react";
|
|
|
|
import type { MutationIntentFactory } from "../../src/application/ports/mutation-intent-factory.ts";
|
|
import { QueryInvalidationProvider } from "../../src/presentation/adapters/query/query-invalidation-provider.tsx";
|
|
import { MutationIntentProvider } from "../../src/presentation/adapters/query/mutation-intent-provider.tsx";
|
|
import {
|
|
defineQueryInvalidationTopic,
|
|
type QueryInvalidationCoordinator,
|
|
} from "../../src/contracts/query-invalidation.ts";
|
|
import { createRuntimeIdentityRegistry } from "../../src/contracts/query-keys.ts";
|
|
import type { QueryResultMeasure } from "../../src/contracts/server-state.ts";
|
|
import type { CacheScopeSnapshot } from "../../src/contracts/server-state-scope.ts";
|
|
|
|
export const RESOURCE_INVALIDATION_TOPIC =
|
|
defineQueryInvalidationTopic("resource");
|
|
|
|
export function scopeSnapshot(
|
|
generation = 1,
|
|
fingerprint = "scope-fingerprint-0001",
|
|
): CacheScopeSnapshot & { fence(): void } {
|
|
let current = true;
|
|
const lifetime = new AbortController();
|
|
const identities = createRuntimeIdentityRegistry({
|
|
tokenFactory: () => "scope-identity-token-0001",
|
|
});
|
|
return {
|
|
generation,
|
|
fingerprint,
|
|
identities,
|
|
signal: lifetime.signal,
|
|
isCurrent: () => current,
|
|
fence() {
|
|
current = false;
|
|
lifetime.abort();
|
|
},
|
|
};
|
|
}
|
|
|
|
export function measureOne(): QueryResultMeasure {
|
|
return { itemCount: 1, estimatedBytes: 8 };
|
|
}
|
|
|
|
export function queryClient() {
|
|
return new QueryClient({
|
|
defaultOptions: {
|
|
queries: { retry: false, staleTime: 0, gcTime: Infinity },
|
|
mutations: { retry: false },
|
|
},
|
|
});
|
|
}
|
|
|
|
export function deterministicMutationIntentFactory(): MutationIntentFactory {
|
|
let sequence = 0;
|
|
return Object.freeze({
|
|
create(input) {
|
|
sequence += 1;
|
|
return Object.freeze({
|
|
intentId: `intent-${sequence}`,
|
|
operationId: input.operationId,
|
|
canonicalInputIdentity: input.canonicalInputIdentity,
|
|
...(input.requiresIdempotencyKey
|
|
? { idempotencyKey: `key-${sequence}` }
|
|
: {}),
|
|
createdAtMonotonicMs: sequence,
|
|
});
|
|
},
|
|
});
|
|
}
|
|
|
|
export function wrapper(
|
|
client: QueryClient,
|
|
mutationIntentFactory = deterministicMutationIntentFactory(),
|
|
) {
|
|
const coordinator: QueryInvalidationCoordinator = {
|
|
async invalidate(topics) {
|
|
for (const topic of topics) {
|
|
await client.invalidateQueries({
|
|
queryKey: [topic],
|
|
exact: false,
|
|
refetchType: "active",
|
|
});
|
|
}
|
|
},
|
|
beginMutation() {
|
|
return { release: async () => {} };
|
|
},
|
|
async resetLocal() {
|
|
await client.cancelQueries();
|
|
client.clear();
|
|
},
|
|
dispose() {},
|
|
};
|
|
return function QueryWrapper({ children }: { children: ReactNode }) {
|
|
return (
|
|
<MutationIntentProvider factory={mutationIntentFactory}>
|
|
<QueryClientProvider client={client}>
|
|
<QueryInvalidationProvider coordinator={coordinator}>
|
|
{children}
|
|
</QueryInvalidationProvider>
|
|
</QueryClientProvider>
|
|
</MutationIntentProvider>
|
|
);
|
|
};
|
|
}
|