Skip to content
Open
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
12 changes: 6 additions & 6 deletions workspaces/client/src/app/createRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function createRoutes(store: ReturnType<typeof createStore>): RouteObject
async lazy() {
const { HomePage, prefetch } = await lazy(
import('@wsh-2025/client/src/pages/home/components/HomePage'),
1000,
100,
);
return {
Component: HomePage,
Expand All @@ -27,7 +27,7 @@ export function createRoutes(store: ReturnType<typeof createStore>): RouteObject
async lazy() {
const { EpisodePage, prefetch } = await lazy(
import('@wsh-2025/client/src/pages/episode/components/EpisodePage'),
1000,
100,
);
return {
Component: EpisodePage,
Expand All @@ -42,7 +42,7 @@ export function createRoutes(store: ReturnType<typeof createStore>): RouteObject
async lazy() {
const { prefetch, ProgramPage } = await lazy(
import('@wsh-2025/client/src/pages/program/components/ProgramPage'),
1000,
100,
);
return {
Component: ProgramPage,
Expand All @@ -57,7 +57,7 @@ export function createRoutes(store: ReturnType<typeof createStore>): RouteObject
async lazy() {
const { prefetch, SeriesPage } = await lazy(
import('@wsh-2025/client/src/pages/series/components/SeriesPage'),
1000,
100,
);
return {
Component: SeriesPage,
Expand All @@ -72,7 +72,7 @@ export function createRoutes(store: ReturnType<typeof createStore>): RouteObject
async lazy() {
const { prefetch, TimetablePage } = await lazy(
import('@wsh-2025/client/src/pages/timetable/components/TimetablePage'),
1000,
100,
);
return {
Component: TimetablePage,
Expand All @@ -87,7 +87,7 @@ export function createRoutes(store: ReturnType<typeof createStore>): RouteObject
async lazy() {
const { NotFoundPage, prefetch } = await lazy(
import('@wsh-2025/client/src/pages/not_found/components/NotFoundPage'),
1000,
100,
);
return {
Component: NotFoundPage,
Expand Down
97 changes: 68 additions & 29 deletions workspaces/client/src/features/episode/services/episodeService.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { createFetch, createSchema } from '@better-fetch/fetch';
import { StandardSchemaV1 } from '@standard-schema/spec';
import * as schema from '@wsh-2025/schema/src/api/schema';
import * as batshit from '@yornaath/batshit';

import { schedulePlugin } from '@wsh-2025/client/src/features/requests/schedulePlugin';

Expand All @@ -15,47 +14,87 @@ const $fetch = createFetch({
},
'/episodes/:episodeId': {
output: schema.getEpisodeByIdResponse,
params: schema.getEpisodeByIdRequestParams,
},
}),
throw: true,
});

const batcher = batshit.create({
async fetcher(queries: { episodeId: string }[]) {
const data = await $fetch('/episodes', {
query: {
episodeIds: queries.map((q) => q.episodeId).join(','),
},
});
return data;
},
resolver(items, query: { episodeId: string }) {
const item = items.find((item) => item.id === query.episodeId);
if (item == null) {
throw new Error('Episode is not found.');
}
return item;
},
scheduler: batshit.windowedFiniteBatchScheduler({
maxBatchSize: 100,
windowMs: 1000,
}),
});

interface EpisodeService {
fetchEpisodeById: (query: {
fetchEpisodeById: (params: {
episodeId: string;
}) => Promise<StandardSchemaV1.InferOutput<typeof schema.getEpisodeByIdResponse>>;
fetchEpisodes: () => Promise<StandardSchemaV1.InferOutput<typeof schema.getEpisodesResponse>>;
fetchEpisodes: (params: {
episodeIds?: string[];
}) => Promise<StandardSchemaV1.InferOutput<typeof schema.getEpisodesResponse>>;
}

// キャッシュの設定
const CACHE_TTL = 5 * 60 * 1000; // キャッシュの有効期限を5分に設定
const episodeCache = new Map<string, StandardSchemaV1.InferOutput<typeof schema.getEpisodeByIdResponse>>();
const cacheTimestamps = new Map<string, number>();

// バッチ処理用のキュー
let batchQueue: string[] = [];
let batchTimeout: NodeJS.Timeout | null = null;
const BATCH_WINDOW = 1; // バッチウィンドウを1msに短縮
const MAX_BATCH_SIZE = 10; // 最大バッチサイズを制限

export const episodeService: EpisodeService = {
async fetchEpisodeById({ episodeId }) {
const channel = await batcher.fetch({ episodeId });
return channel;
// キャッシュをチェック
const cachedEpisode = episodeCache.get(episodeId);
const timestamp = cacheTimestamps.get(episodeId);
if (cachedEpisode && timestamp && Date.now() - timestamp < CACHE_TTL) {
return cachedEpisode;
}

// バッチキューに追加
batchQueue.push(episodeId);

// 既存のタイマーをクリア
if (batchTimeout) {
clearTimeout(batchTimeout);
}

// 新しいタイマーを設定
return new Promise((resolve, reject) => {
batchTimeout = setTimeout(async () => {
// 重複を除去し、最大サイズを制限
const uniqueIds = [...new Set(batchQueue)].slice(0, MAX_BATCH_SIZE);

try {
const episodes = await this.fetchEpisodes({ episodeIds: uniqueIds });

// キャッシュを更新
const now = Date.now();
episodes.forEach(episode => {
episodeCache.set(episode.id, episode);
cacheTimestamps.set(episode.id, now);
});

const episode = episodes.find(e => e.id === episodeId);
if (!episode) {
reject(new Error(`Episode not found: ${episodeId}`));
return;
}
resolve(episode);
} catch (error) {
console.error('Error fetching episodes:', error);
reject(error);
} finally {
batchQueue = [];
}
}, BATCH_WINDOW);
});
},
async fetchEpisodes() {
const data = await $fetch('/episodes', { query: {} });

async fetchEpisodes({ episodeIds }) {
if (!episodeIds?.length) return [];

const data = await $fetch('/episodes', {
query: { episodeIds: episodeIds.join(',') },
});
return data;
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ const batcher = batshit.create({
return item;
},
scheduler: batshit.windowedFiniteBatchScheduler({
maxBatchSize: 100,
windowMs: 1000,
maxBatchSize: 500,
windowMs: 0,
}),
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const $fetch = createFetch({
schema: createSchema({
'/recommended/:referenceId': {
output: schema.getRecommendedModulesResponse,
params: schema.getRecommendedModulesRequestParams,
},
}),
throw: true,
Expand All @@ -21,11 +22,63 @@ interface RecommendedService {
}) => Promise<StandardSchemaV1.InferOutput<typeof schema.getRecommendedModulesResponse>>;
}

// キャッシュの設定
const CACHE_TTL = 5 * 60 * 1000; // キャッシュの有効期限を5分に設定
const recommendedCache = new Map<string, StandardSchemaV1.InferOutput<typeof schema.getRecommendedModulesResponse>>();
const cacheTimestamps = new Map<string, number>();

// バッチ処理用のキュー
let batchQueue: string[] = [];
let batchTimeout: NodeJS.Timeout | null = null;
const BATCH_WINDOW = 0; // バッチウィンドウを0msに設定(即時実行)
const MAX_BATCH_SIZE = 5; // 最大バッチサイズを制限

// プリフェッチ用のキュー
const prefetchQueue = new Set<string>();

export const recommendedService: RecommendedService = {
async fetchRecommendedModulesByReferenceId({ referenceId }) {
const data = await $fetch('/recommended/:referenceId', {
params: { referenceId },
// キャッシュをチェック
const cachedData = recommendedCache.get(referenceId);
const timestamp = cacheTimestamps.get(referenceId);
if (cachedData && timestamp && Date.now() - timestamp < CACHE_TTL) {
return cachedData;
}

// プリフェッチキューに追加
prefetchQueue.add(referenceId);

// バッチキューに追加
batchQueue.push(referenceId);

// 既存のタイマーをクリア
if (batchTimeout) {
clearTimeout(batchTimeout);
}

// 新しいタイマーを設定
return new Promise((resolve, reject) => {
batchTimeout = setTimeout(async () => {
// 重複を除去し、最大サイズを制限
const uniqueIds = [...new Set(batchQueue)].slice(0, MAX_BATCH_SIZE);

try {
const data = await $fetch('/recommended/:referenceId', {
params: { referenceId },
});

// キャッシュを更新
recommendedCache.set(referenceId, data);
cacheTimestamps.set(referenceId, Date.now());

resolve(data);
} catch (error) {
console.error('Error fetching recommended modules:', error);
reject(error);
} finally {
batchQueue = [];
}
}, BATCH_WINDOW);
});
return data;
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { create } from 'zustand';
import { StandardSchemaV1 } from '@standard-schema/spec';
import * as schema from '@wsh-2025/schema/src/api/schema';
import { recommendedService } from '../services/recommendedService';

type RecommendedState = {
modules: StandardSchemaV1.InferOutput<typeof schema.getRecommendedModulesResponse> | null;
isLoading: boolean;
error: Error | null;
fetchModules: (referenceId: string) => Promise<void>;
};

export const useRecommendedStore = create<RecommendedState>((set) => ({
modules: null,
isLoading: false,
error: null,
fetchModules: async (referenceId: string) => {
try {
set({ isLoading: true, error: null });
const modules = await recommendedService.fetchRecommendedModulesByReferenceId(referenceId);
set({ modules, isLoading: false });
} catch (error) {
set({ error: error as Error, isLoading: false });
}
},
}));
8 changes: 2 additions & 6 deletions workspaces/client/src/features/requests/schedulePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,9 @@ export const schedulePlugin = {
const scheduler = typeof window !== 'undefined' ? window.scheduler : undefined;

if (scheduler) {
return await scheduler.postTask(() => request, { delay: 1000 });
return await scheduler.postTask(() => request);
} else {
return await new Promise<typeof request>((resolve) => {
queueMicrotask(() => {
resolve(request);
});
});
return request;
}
},
},
Expand Down
5 changes: 4 additions & 1 deletion workspaces/client/src/pages/home/components/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import { useRecommended } from '@wsh-2025/client/src/features/recommended/hooks/
export const prefetch = async (store: ReturnType<typeof createStore>) => {
const modules = await store
.getState()
.features.recommended.fetchRecommendedModulesByReferenceId({ referenceId: 'entrance' });
.features.recommended.fetchRecommendedModulesByReferenceId({
referenceId: 'entrance',
limit: 1 // 最初のモジュールのみを取得
});
return { modules };
};

Expand Down
Loading