feat: set up mock data to be used to generate baseline profiles

This commit is contained in:
2025-12-06 07:27:14 -05:00
parent 9d71199943
commit ad26cd6da3
9 changed files with 239 additions and 62 deletions

View File

@@ -49,6 +49,17 @@ export function fetchEpisodes(aniListId: number, shouldRetry: boolean = false) {
app.openapi(route, async (c) => {
const aniListId = Number(c.req.param("aniListId"));
// Check if we should use mock data
const { useMockData } = await import("~/libs/useMockData");
if (useMockData()) {
const { mockEpisodes } = await import("~/mocks/mockData");
return c.json({
success: true,
result: { providerId: "aniwatch", episodes: mockEpisodes() },
});
}
const episodes = await fetchEpisodes(aniListId);
if (episodes.length === 0) {
return c.json(ErrorResponse, { status: 404 });

View File

@@ -120,6 +120,14 @@ app.openapi(route, async (c) => {
return c.json(ErrorResponse, { status: 400 });
}
// Check if we should use mock data
const { useMockData } = await import("~/libs/useMockData");
if (useMockData()) {
const { mockEpisodeUrl } = await import("~/mocks/mockData");
return c.json({ success: true, result: mockEpisodeUrl });
}
try {
console.log(
`Fetching episode URL for aniListId: ${aniListId}, episodeNumber: ${episodeNumber}`,

View File

@@ -38,6 +38,27 @@ app.openapi(route, async (c) => {
const page = Number(c.req.query("page") ?? 1);
const limit = Number(c.req.query("limit") ?? 10);
// Check if we should use mock data
const { useMockData } = await import("~/libs/useMockData");
if (useMockData()) {
const { mockSearchResults } = await import("~/mocks/mockData");
// Paginate mock results
const startIndex = (page - 1) * limit;
const endIndex = startIndex + limit;
const paginatedResults = mockSearchResults.slice(startIndex, endIndex);
const hasNextPage = endIndex < mockSearchResults.length;
return c.json(
{
success: true,
results: paginatedResults,
hasNextPage,
},
200,
);
}
const { result: response, errorOccurred } = await fetchFromMultipleSources([
() => fetchSearchResultsFromAnilist(query, page, limit),
]);

View File

@@ -46,6 +46,14 @@ app.openapi(route, async (c) => {
const aniListId = Number(c.req.query("id"));
const aniListToken = c.req.header("X-AniList-Token");
// Check if we should use mock data
const { useMockData } = await import("~/libs/useMockData");
if (useMockData()) {
const { mockTitleDetails } = await import("~/mocks/mockData");
return c.json({ success: true, result: mockTitleDetails() }, 200);
}
const { result: title, errorOccurred } = await fetchFromMultipleSources([
() => fetchTitleFromAnilist(aniListId, aniListToken ?? undefined),
]);

View File

@@ -92,6 +92,13 @@ app.openapi(route, async (c) => {
} = await c.req.json<typeof UpdateWatchStatusRequest._type>();
const aniListToken = c.req.header("X-AniList-Token");
// Check if we should use mock data
const { useMockData } = await import("~/libs/useMockData");
if (useMockData()) {
// Return success immediately without side effects
return c.json(SuccessResponse, { status: 200 });
}
if (!isRetrying) {
try {
await updateWatchStatus(c.req, deviceId, titleId, watchStatus);

9
src/libs/useMockData.ts Normal file
View File

@@ -0,0 +1,9 @@
import { env } from "cloudflare:workers";
/**
* Check if mock data should be used based on environment variable
* @returns true if USE_MOCK_DATA environment variable is set to "true"
*/
export function useMockData(): boolean {
return env.USE_MOCK_DATA == "true";
}

103
src/mocks/mockData.ts Normal file
View File

@@ -0,0 +1,103 @@
import type { FetchUrlResponseSchema } from "~/types/episode/fetch-url-response";
import type { Title } from "~/types/title";
import type { HomeTitle } from "~/types/title/homeTitle";
/**
* Mock data for search results
*/
export const mockSearchResults: HomeTitle[] = [
{
id: 151807,
title: "Solo Leveling",
coverImage: {
extraLarge:
"https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx151807-yxY3olrjZH4k.png",
large:
"https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx151807-yxY3olrjZH4k.png",
medium:
"https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx151807-yxY3olrjZH4k.png",
},
},
{
id: 139589,
title: "Kotaro Lives Alone",
coverImage: {
extraLarge:
"https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx139589-oFz7JwpwRkQV.png",
large:
"https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx139589-oFz7JwpwRkQV.png",
medium:
"https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx139589-oFz7JwpwRkQV.png",
},
},
{
id: 176496,
title: "Solo Leveling Season 2 -Arise from the Shadow-",
coverImage: {
extraLarge:
"https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx176496-r6oXxEqdZL0n.jpg",
large:
"https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx176496-r6oXxEqdZL0n.jpg",
medium:
"https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx176496-r6oXxEqdZL0n.jpg",
},
},
];
/**
* Mock data for title details
*/
export const mockTitleDetails: () => Title = () => ({
id: Math.floor(Math.random() * 1000000),
idMal: Math.floor(Math.random() * 1000000),
title: {
userPreferred: "The Grimm Variations",
english: "The Grimm Variations",
},
description:
'Once upon a time, brothers Jacob and Wilhelm collected fairy tales from across the land and made them into a book. They also had a much younger sister, the innocent and curious Charlotte, who they loved very much. One day, while the brothers were telling Charlotte a fairy tale like usual, they saw that she had a somewhat melancholy look on her face. She asked them, "Do you suppose they really lived happily ever after?"<br><br>\nThe pages of Grimms\' Fairy Tales, written by Jacob and Wilhelm, are now presented from the unique perspective of Charlotte, who sees the stories quite differently from her brothers.<br><br>\n(Source: Netflix Anime)',
episodes: 6,
genres: ["Fantasy", "Thriller"],
status: "FINISHED",
bannerImage:
"https://s4.anilist.co/file/anilistcdn/media/anime/banner/135643-cmQZCR3z9dB5.jpg",
averageScore: 66,
coverImage: {
extraLarge:
"https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx135643-2kJt86K9Db9P.jpg",
large:
"https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx135643-2kJt86K9Db9P.jpg",
medium:
"https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx135643-2kJt86K9Db9P.jpg",
},
countryOfOrigin: "JP",
mediaListEntry: null,
nextAiringEpisode: null,
});
/**
* Mock data for episode URL
* Using Big Buck Bunny test video
*/
export const mockEpisodeUrl: FetchUrlResponseSchema = {
source: "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8",
subtitles: [],
audio: [],
intro: null,
outro: null,
headers: {},
};
/**
* Mock data for episodes list
* Returns a sample list of 50 episodes for testing
*/
export const mockEpisodes = () => {
const randomId = Math.floor(Math.random() * 1000000);
return Array.from({ length: 50 }, (_, i) => ({
id: `${randomId}-episode-${i + 1}`,
number: i + 1,
title: `Episode ${i + 1}`,
isFiller: false,
}));
};

View File

@@ -1,5 +1,5 @@
/* eslint-disable */
// Generated by Wrangler by running `wrangler types` (hash: e13701d63777919829c99954d077e310)
// Generated by Wrangler by running `wrangler types` (hash: 90bb6a4d2794b1db56ddb3ed3013672a)
// Runtime types generated with workerd@1.20251125.0 2025-11-28 nodejs_compat
declare namespace Cloudflare {
interface GlobalProps {
@@ -15,6 +15,7 @@ declare namespace Cloudflare {
CLOUDFLARE_ACCOUNT_ID: string;
CLOUDFLARE_DATABASE_ID: string;
PROXY_URL: string;
USE_MOCK_DATA: string;
ANILIST_DO: DurableObjectNamespace<import("./src/index").AnilistDo>;
DB: D1Database;
ANILIST_UPDATES: Queue;
@@ -26,7 +27,7 @@ type StringifyValues<EnvType extends Record<string, unknown>> = {
[Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string;
};
declare namespace NodeJS {
interface ProcessEnv extends StringifyValues<Pick<Cloudflare.Env, "ENABLE_ANIFY" | "ADMIN_SDK_JSON" | "CLOUDFLARE_TOKEN" | "CLOUDFLARE_D1_TOKEN" | "CLOUDFLARE_ACCOUNT_ID" | "CLOUDFLARE_DATABASE_ID" | "PROXY_URL">> {}
interface ProcessEnv extends StringifyValues<Pick<Cloudflare.Env, "ENABLE_ANIFY" | "ADMIN_SDK_JSON" | "CLOUDFLARE_TOKEN" | "CLOUDFLARE_D1_TOKEN" | "CLOUDFLARE_ACCOUNT_ID" | "CLOUDFLARE_DATABASE_ID" | "PROXY_URL" | "USE_MOCK_DATA">> {}
}
// Begin runtime types
@@ -1643,7 +1644,7 @@ declare abstract class Body {
*/
declare var Response: {
prototype: Response;
new (body?: BodyInit | null, init?: ResponseInit): Response;
new(body?: BodyInit | null, init?: ResponseInit): Response;
error(): Response;
redirect(url: string, status?: number): Response;
json(any: any, maybeInit?: (ResponseInit | Response)): Response;
@@ -2191,7 +2192,7 @@ interface ReadableStream<R = any> {
*/
declare const ReadableStream: {
prototype: ReadableStream;
new (underlyingSource: UnderlyingByteSource, strategy?: QueuingStrategy<Uint8Array>): ReadableStream<Uint8Array>;
new(underlyingSource: UnderlyingByteSource, strategy?: QueuingStrategy<Uint8Array>): ReadableStream<Uint8Array>;
new <R = any>(underlyingSource?: UnderlyingSource<R>, strategy?: QueuingStrategy<R>): ReadableStream<R>;
};
/**
@@ -3033,7 +3034,7 @@ type WebSocketEventMap = {
*/
declare var WebSocket: {
prototype: WebSocket;
new (url: string, protocols?: (string[] | string)): WebSocket;
new(url: string, protocols?: (string[] | string)): WebSocket;
readonly READY_STATE_CONNECTING: number;
readonly CONNECTING: number;
readonly READY_STATE_OPEN: number;
@@ -3090,7 +3091,7 @@ interface WebSocket extends EventTarget<WebSocketEventMap> {
extensions: string | null;
}
declare const WebSocketPair: {
new (): {
new(): {
0: WebSocket;
1: WebSocket;
};
@@ -9412,21 +9413,21 @@ interface IncomingRequestCfPropertiesTLSClientAuthPlaceholder {
certNotAfter: "";
}
/** Possible outcomes of TLS verification */
declare type CertVerificationStatus =
/** Authentication succeeded */
"SUCCESS"
/** No certificate was presented */
| "NONE"
/** Failed because the certificate was self-signed */
| "FAILED:self signed certificate"
/** Failed because the certificate failed a trust chain check */
| "FAILED:unable to verify the first certificate"
/** Failed because the certificate not yet valid */
| "FAILED:certificate is not yet valid"
/** Failed because the certificate is expired */
| "FAILED:certificate has expired"
/** Failed for another unspecified reason */
| "FAILED";
declare type CertVerificationStatus =
/** Authentication succeeded */
"SUCCESS"
/** No certificate was presented */
| "NONE"
/** Failed because the certificate was self-signed */
| "FAILED:self signed certificate"
/** Failed because the certificate failed a trust chain check */
| "FAILED:unable to verify the first certificate"
/** Failed because the certificate not yet valid */
| "FAILED:certificate is not yet valid"
/** Failed because the certificate is expired */
| "FAILED:certificate has expired"
/** Failed for another unspecified reason */
| "FAILED";
/**
* An upstream endpoint's response to a TCP `keepalive` message from Cloudflare.
*/
@@ -9476,15 +9477,15 @@ interface D1ExecResult {
count: number;
duration: number;
}
type D1SessionConstraint =
// Indicates that the first query should go to the primary, and the rest queries
// using the same D1DatabaseSession will go to any replica that is consistent with
// the bookmark maintained by the session (returned by the first query).
'first-primary'
// Indicates that the first query can go anywhere (primary or replica), and the rest queries
// using the same D1DatabaseSession will go to any replica that is consistent with
// the bookmark maintained by the session (returned by the first query).
| 'first-unconstrained';
type D1SessionConstraint =
// Indicates that the first query should go to the primary, and the rest queries
// using the same D1DatabaseSession will go to any replica that is consistent with
// the bookmark maintained by the session (returned by the first query).
'first-primary'
// Indicates that the first query can go anywhere (primary or replica), and the rest queries
// using the same D1DatabaseSession will go to any replica that is consistent with
// the bookmark maintained by the session (returned by the first query).
| 'first-unconstrained';
type D1SessionBookmark = string;
declare abstract class D1Database {
prepare(query: string): D1PreparedStatement;
@@ -9598,7 +9599,7 @@ declare type EmailExportedHandler<Env = unknown> = (message: ForwardableEmailMes
declare module "cloudflare:email" {
let _EmailMessage: {
prototype: EmailMessage;
new (from: string, to: string, raw: ReadableStream | string): EmailMessage;
new(from: string, to: string, raw: ReadableStream | string): EmailMessage;
};
export { _EmailMessage as EmailMessage };
}
@@ -10057,17 +10058,17 @@ declare namespace Rpc {
// The reason for using a generic type here is to build a serializable subset of structured
// cloneable composite types. This allows types defined with the "interface" keyword to pass the
// serializable check as well. Otherwise, only types defined with the "type" keyword would pass.
type Serializable<T> =
// Structured cloneables
BaseType
// Structured cloneable composites
| Map<T extends Map<infer U, unknown> ? Serializable<U> : never, T extends Map<unknown, infer U> ? Serializable<U> : never> | Set<T extends Set<infer U> ? Serializable<U> : never> | ReadonlyArray<T extends ReadonlyArray<infer U> ? Serializable<U> : never> | {
[K in keyof T]: K extends number | string ? Serializable<T[K]> : never;
}
// Special types
| Stub<Stubable>
// Serialized as stubs, see `Stubify`
| Stubable;
type Serializable<T> =
// Structured cloneables
BaseType
// Structured cloneable composites
| Map<T extends Map<infer U, unknown> ? Serializable<U> : never, T extends Map<unknown, infer U> ? Serializable<U> : never> | Set<T extends Set<infer U> ? Serializable<U> : never> | ReadonlyArray<T extends ReadonlyArray<infer U> ? Serializable<U> : never> | {
[K in keyof T]: K extends number | string ? Serializable<T[K]> : never;
}
// Special types
| Stub<Stubable>
// Serialized as stubs, see `Stubify`
| Stubable;
// Base type for all RPC stubs, including common memory management methods.
// `T` is used as a marker type for unwrapping `Stub`s later.
interface StubBase<T extends Stubable> extends Disposable {
@@ -10082,8 +10083,8 @@ declare namespace Rpc {
type Stubify<T> = T extends Stubable ? Stub<T> : T extends Map<infer K, infer V> ? Map<Stubify<K>, Stubify<V>> : T extends Set<infer V> ? Set<Stubify<V>> : T extends Array<infer V> ? Array<Stubify<V>> : T extends ReadonlyArray<infer V> ? ReadonlyArray<Stubify<V>> : T extends BaseType ? T : T extends {
[key: string | number]: any;
} ? {
[K in keyof T]: Stubify<T[K]>;
} : T;
[K in keyof T]: Stubify<T[K]>;
} : T;
// Recursively rewrite all `Stub<T>`s with the corresponding `T`s.
// Note we use `StubBase` instead of `Stub` here to avoid circular dependencies:
// `Stub` depends on `Provider`, which depends on `Unstubify`, which would depend on `Stub`.
@@ -10091,8 +10092,8 @@ declare namespace Rpc {
type Unstubify<T> = T extends StubBase<infer V> ? V : T extends Map<infer K, infer V> ? Map<Unstubify<K>, Unstubify<V>> : T extends Set<infer V> ? Set<Unstubify<V>> : T extends Array<infer V> ? Array<Unstubify<V>> : T extends ReadonlyArray<infer V> ? ReadonlyArray<Unstubify<V>> : T extends BaseType ? T : T extends {
[key: string | number]: unknown;
} ? {
[K in keyof T]: Unstubify<T[K]>;
} : T;
[K in keyof T]: Unstubify<T[K]>;
} : T;
type UnstubifyAll<A extends any[]> = {
[I in keyof A]: Unstubify<A[I]>;
};
@@ -10165,7 +10166,7 @@ declare namespace Cloudflare {
[K in keyof MainModule]: LoopbackForExport<MainModule[K]>
// If the export is listed in `durableNamespaces`, then it is also a
// DurableObjectNamespace.
& (K extends GlobalProp<"durableNamespaces", never> ? MainModule[K] extends new (...args: any[]) => infer DoInstance ? DoInstance extends Rpc.DurableObjectBranded ? DurableObjectNamespace<DoInstance> : DurableObjectNamespace<undefined> : DurableObjectNamespace<undefined> : {});
& (K extends GlobalProp<"durableNamespaces", never> ? MainModule[K] extends new (...args: any[]) => infer DoInstance ? DoInstance extends Rpc.DurableObjectBranded ? DurableObjectNamespace<DoInstance> : DurableObjectNamespace<undefined> : DurableObjectNamespace<undefined> : {});
};
}
declare namespace CloudflareWorkersModule {
@@ -10821,10 +10822,10 @@ interface WorkflowInstanceCreateOptions<PARAMS = unknown> {
}
type InstanceStatus = {
status: 'queued' // means that instance is waiting to be started (see concurrency limits)
| 'running' | 'paused' | 'errored' | 'terminated' // user terminated the instance while it was running
| 'complete' | 'waiting' // instance is hibernating and waiting for sleep or event to finish
| 'waitingForPause' // instance is finishing the current work to pause
| 'unknown';
| 'running' | 'paused' | 'errored' | 'terminated' // user terminated the instance while it was running
| 'complete' | 'waiting' // instance is hibernating and waiting for sleep or event to finish
| 'waitingForPause' // instance is finishing the current work to pause
| 'unknown';
error?: string;
output?: object;
};

View File

@@ -6,6 +6,15 @@ compatibility_date = "2025-11-28"
[vars]
ENABLE_ANIFY = false
USE_MOCK_DATA = false
[env.staging]
[env.staging.vars]
USE_MOCK_DATA = true
[[env.staging.durable_objects.bindings]]
name = "ANILIST_DO"
class_name = "AnilistDo"
[observability]
enabled = true
@@ -16,20 +25,20 @@ class_name = "AnilistDo"
[[migrations]]
tag = "v1"
new_classes = ["AniwatchApiContainer"]
[[migrations]]
tag = "<v2>"
deleted_classes = ["AniwatchApiContainer"]
new_classes = ["AnilistDo"]
[[migrations]]
tag = "v3"
deleted_classes = ["AnilistDo"]
#[[migrations]]
#tag = "<v2>"
#deleted_classes = ["AniwatchApiContainer"]
#new_classes = ["AnilistDo"]
[[migrations]]
tag = "v4"
new_sqlite_classes = ["AnilistDo"]
#[[migrations]]
#tag = "v3"
#deleted_classes = ["AnilistDo"]
#[[migrations]]
#tag = "v4"
#new_sqlite_classes = ["AnilistDo"]
[[queues.producers]]
queue = "anilist-updates"