Skip to content
Draft
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
232 changes: 205 additions & 27 deletions .pnp.cjs

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,32 @@
"build:js": "node ./esbuild.config.js",
"clean": "rimraf dist",
"dev": "yarn build:js --watch && yarn build:dts --watch",
"test": "yarn jest",
"typecheck": "tsc --noEmit"
},
"jest": {
"coveragePathIgnorePatterns": [
"index.ts"
],
"transform": {
"^.+\\.(t|j)sx?$": "@swc/jest"
}
},
"devDependencies": {
"@stackflow/core": "^1.3.0",
"@stackflow/esbuild-config": "^1.0.3",
"@swc/core": "^1.6.6",
"@swc/jest": "^0.2.36",
"@types/jest": "^29.5.12",
"esbuild": "^0.23.0",
"jest": "^29.7.0",
"rimraf": "^3.0.2",
"typescript": "^5.5.3",
"ultra-runner": "^3.10.5"
},
"peerDependencies": {
"@stackflow/core": "^1.1.0-canary.0"
},
"publishConfig": {
"access": "public"
},
Expand Down
4 changes: 2 additions & 2 deletions config/src/ActivityDefinition.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { ActivityLoader } from "./ActivityLoader";
import type { ActivityLoaderConfig } from "./ActivityLoader";
import type { RegisteredActivityName } from "./RegisteredActivityName";

export interface ActivityDefinition<
ActivityName extends RegisteredActivityName,
> {
name: ActivityName;
loader?: ActivityLoader<any>;
loader?: ActivityLoaderConfig<any>;
}
98 changes: 98 additions & 0 deletions config/src/ActivityLoader.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type { Activity } from "@stackflow/core";
import { getLoaderFn, getShouldInvalidate, loader } from "./ActivityLoader";

describe("getLoaderFn", () => {
it("should return undefined when loaderConfig is undefined", () => {
expect(getLoaderFn(undefined)).toBeUndefined();
});

it("should return the function itself when loaderConfig is a function", () => {
const loaderFn = () => Promise.resolve({ data: "test" });
expect(getLoaderFn(loaderFn)).toBe(loaderFn);
});

it("should return the fn property when loaderConfig is an object", () => {
const loaderFn = () => Promise.resolve({ data: "test" });
const loaderConfig = {
fn: loaderFn,
shouldInvalidate: () => true,
};
expect(getLoaderFn(loaderConfig)).toBe(loaderFn);
});

it("should return the fn property when loaderConfig object has no shouldInvalidate", () => {
const loaderFn = () => Promise.resolve({ data: "test" });
const loaderConfig = { fn: loaderFn };
expect(getLoaderFn(loaderConfig)).toBe(loaderFn);
});
});

describe("getShouldInvalidate", () => {
it("should return undefined when loaderConfig is undefined", () => {
expect(getShouldInvalidate(undefined)).toBeUndefined();
});

it("should return undefined when loaderConfig is a function", () => {
const loaderFn = () => Promise.resolve({ data: "test" });
expect(getShouldInvalidate(loaderFn)).toBeUndefined();
});

it("should return the shouldInvalidate function when loaderConfig is an object", () => {
const shouldInvalidateFn = ({
prevActivity,
currentActivity,
}: {
prevActivity: Activity;
currentActivity: Activity;
}) => !prevActivity.isActive && currentActivity.isActive;

const loaderConfig = {
fn: () => Promise.resolve({ data: "test" }),
shouldInvalidate: shouldInvalidateFn,
};
expect(getShouldInvalidate(loaderConfig)).toBe(shouldInvalidateFn);
});

it("should return undefined when loaderConfig object has no shouldInvalidate", () => {
const loaderConfig = {
fn: () => Promise.resolve({ data: "test" }),
};
expect(getShouldInvalidate(loaderConfig)).toBeUndefined();
});
});

describe("loader", () => {
it("should return the function directly when no options provided", () => {
const loaderFn = () => Promise.resolve({ data: "test" });
const result = loader(loaderFn);
expect(result).toBe(loaderFn);
});

it("should return ActivityLoaderConfigObject when options provided", () => {
const loaderFn = () => Promise.resolve({ data: "test" });
const shouldInvalidateFn = ({
prevActivity,
currentActivity,
}: {
prevActivity: Activity;
currentActivity: Activity;
}) => !prevActivity.isActive && currentActivity.isActive;

const result = loader(loaderFn, { shouldInvalidate: shouldInvalidateFn });

expect(result).toEqual({
fn: loaderFn,
shouldInvalidate: shouldInvalidateFn,
});
});

it("should work with getLoaderFn and getShouldInvalidate", () => {
const loaderFn = () => Promise.resolve({ data: "test" });
const shouldInvalidateFn = () => true;

const config = loader(loaderFn, { shouldInvalidate: shouldInvalidateFn });

expect(getLoaderFn(config)).toBe(loaderFn);
expect(getShouldInvalidate(config)).toBe(shouldInvalidateFn);
});
});
64 changes: 62 additions & 2 deletions config/src/ActivityLoader.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,72 @@
import type { Activity } from "@stackflow/core";

import type { ActivityLoaderArgs } from "./ActivityLoaderArgs";
import type { RegisteredActivityName } from "./RegisteredActivityName";

export type ActivityLoader<ActivityName extends RegisteredActivityName> = (
args: ActivityLoaderArgs<ActivityName>,
) => any;

export interface LoaderOptions<ActivityName extends RegisteredActivityName> {
shouldInvalidate?: (args: {
prevActivity: Activity;
currentActivity: Activity;
}) => boolean;
}

export function loader<ActivityName extends RegisteredActivityName>(
loaderFn: (args: ActivityLoaderArgs<ActivityName>) => any,
): ActivityLoader<ActivityName>;

export function loader<ActivityName extends RegisteredActivityName>(
loaderFn: (args: ActivityLoaderArgs<ActivityName>) => any,
options: LoaderOptions<ActivityName>,
): ActivityLoaderConfigObject<ActivityName>;

export function loader<ActivityName extends RegisteredActivityName>(
loaderFn: (args: ActivityLoaderArgs<ActivityName>) => any,
): ActivityLoader<ActivityName> {
return (args: ActivityLoaderArgs<ActivityName>) => loaderFn(args);
options?: LoaderOptions<ActivityName>,
): ActivityLoader<ActivityName> | ActivityLoaderConfigObject<ActivityName> {
if (options) {
return {
fn: loaderFn,
shouldInvalidate: options.shouldInvalidate,
};
}
return loaderFn;
}

export interface ActivityLoaderConfigObject<
ActivityName extends RegisteredActivityName,
> {
fn: ActivityLoader<ActivityName>;
shouldInvalidate?: (args: {
prevActivity: Activity;
currentActivity: Activity;
}) => boolean;
}

export type ActivityLoaderConfig<ActivityName extends RegisteredActivityName> =
| ActivityLoader<ActivityName>
| ActivityLoaderConfigObject<ActivityName>;

export function getLoaderFn<ActivityName extends RegisteredActivityName>(
loaderConfig: ActivityLoaderConfig<ActivityName> | undefined,
): ActivityLoader<ActivityName> | undefined {
if (!loaderConfig) {
return undefined;
}
if (typeof loaderConfig === "function") {
return loaderConfig;
}
return loaderConfig.fn;
}

export function getShouldInvalidate<ActivityName extends RegisteredActivityName>(
loaderConfig: ActivityLoaderConfig<ActivityName> | undefined,
): ActivityLoaderConfigObject<ActivityName>["shouldInvalidate"] | undefined {
if (!loaderConfig || typeof loaderConfig === "function") {
return undefined;
}
return loaderConfig.shouldInvalidate;
}
Loading
Loading