-
-
Notifications
You must be signed in to change notification settings - Fork 172
feat: package likes #712
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: package likes #712
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
2 Skipped Deployments
|
Adebesin-Cell
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting PR!
Screen.Recording.2026-02-02.at.08.07.12.mov
Connecting with any of the socials results in a 404 error.
It attempts to navigate to: https://npmxdev-git-fork-fatfingers23-feat-likes-poetry.vercel.app/package/api/auth/atproto
app/pages/package/[...package].vue
Outdated
| if (user.value?.handle == null) { | ||
| authModal.open() | ||
| } else { | ||
| const result = likesData.value?.userHasLiked ? await unlikePackage() : await likePackage() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this approach could be improved a bit. Right now, the API request is sent immediately on every click.
It might be better to optimistically update the UI and debounce the request, so users can toggle the button freely and only the final state is sent to the API after a short delay. This would also allow us to provide immediate feedback that the action was registered, and handle request failures more gracefully (e.g., by rolling back the UI state or showing an error).
This would avoid unnecessary API calls and result in a smoother UX overall.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i agree, optimistic updates would fit nicely here and shouldn't be too difficult 👍
server/api/social/like.post.ts
Outdated
| if (hasLiked) { | ||
| throw createError({ | ||
| status: 400, | ||
| message: 'User has already liked the package', | ||
| }) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: silently succeed here instead of throwing an error.
Scenario: user likes the package twice with quirky internet connection and/or slower than usual response times.
Potential result on the UI: user see the like added then seeing the error message that liking has failed.
Been there, done that, with an app with more than 500,000 users :D
server/api/auth/social/like.post.ts
Outdated
| const body = await readBody<{ packageName: string }>(event) | ||
|
|
||
| if (!body.packageName) { | ||
| throw createError({ | ||
| status: 400, | ||
| message: 'packageName is required', | ||
| }) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could leverage valibot here, this way you wouldn't have to resort to custom null checks to ensure that body is actually of type you expect it to be.
| * Gets the definite answer if the user has liked a npm package. Either from the cache or the network | ||
| * @param packageName | ||
| * @param usersDid | ||
| * @returns |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👀
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about making xrpc routes (sth like /xrpc/npmx.feed.like.create)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now we're just using the the server side oauth client so if we made this endpoint an XRPC it wouldn't be quite correct since it uses the cookie for authentication. But there is a plan for some XRPC endpoints for other things just need to make a middleware for it to authenticate the service auth jwt
| driver: 'fsLite', | ||
| base: './.cache/atproto-oauth/session', | ||
| }, | ||
| 'generic-cache': { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we probably also need to update modules/cache.ts to configure production redis
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May have to leave that one to you 😅. Do you have something to read up on that? The local generic-cache defined there is used just locally for dev and then uses upstash redis like you did for the oauth session lock when in production if that makes a difference.
server/utils/atproto/utils/likes.ts
Outdated
| @@ -0,0 +1,246 @@ | |||
| import { getCacheAdatper } from '../../cache' | |||
| import { $nsid as likeNsid } from '#shared/types/lexicons/dev/npmx/feed/like.defs' | |||
| import type { Backlink } from '~~/shared/utils/constellation' | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here and some other places too:
| import type { Backlink } from '~~/shared/utils/constellation' | |
| import type { Backlink } from '#shared/utils/constellation' |
| const authModal = useModal('auth-modal') | ||
|
|
||
| const { data: likesData } = useFetch(() => `/api/social/likes/${packageName.value}`, { | ||
| default: () => ({ totalLikes: 0, userHasLiked: false }), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should probably be client-side only:
| default: () => ({ totalLikes: 0, userHasLiked: false }), | |
| default: () => ({ totalLikes: 0, userHasLiked: false }), | |
| server: false, |
3807cb3 to
3b8d928
Compare
app/composables/useAtproto.ts
Outdated
| import type { PackageLikes } from '#server/utils/atproto/utils/likes' | ||
|
|
||
| export async function authRedirect(identifier: string, create: boolean = false) { | ||
| let query = { handle: identifier } as {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: this should be a type annotation rather than a cast, and shouldn't be {}
| let query = { handle: identifier } as {} | |
| let query: LocationQueryRaw = { handle: identifier } |
or if we can't pull the exact type from vue-router, use Record<string, string> or something
app/composables/useAtproto.ts
Outdated
| } | ||
|
|
||
| export async function handleAuthError(e: unknown, userHandle?: string | null): Promise<never> { | ||
| const fetchError = e as FetchError |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
given we cast the error anyway at all call sites, can this function just accept a FetchError
otherwise we're doing an unsafe cast here, if anyone in future calls it from somewhere that doesn't only do a $fetch in the try block
app/composables/useAtproto.ts
Outdated
| return { user, pending, logout } | ||
| } | ||
|
|
||
| export function useLikePackage(packageName: string) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this file is becoming a bit of a "catch all" as the atproto features quickly expand. we should split them up i think, and have a app/composables/atproto/*, one file per composable.
that'll also help with organising the tests once they exist
its currently named after one of the composables, and the rest are just thrown into the mix
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think all the social/ endpoints need to live outside of the auth/ directory
they're not auth related, they just rely on a session.
auth/*anything to do with actually authenticating (login, logout, session, etc)social/*anything to do with social, whether it requires a session or not
| loggedInUsersDid, | ||
| ) | ||
|
|
||
| if (getTheUsersLikedRecord) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what if it isn't truthy? somehow
feels like something would have to be very wrong for that to happen, but we should log an error if so
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same. I think some of this like logic is kind of "let's see what all comes of it stage". Do we have something for logging or is it just console.error?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think just a regular console.error for now
They should show up in the vercel logs
server/utils/cache.ts
Outdated
| /** | ||
| * Local implmentation of a cache to be used during development | ||
| */ | ||
| export class StorageCacheAdapter implements CacheAdapter { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should probably organise these into files:
server/utils/cache/adapter.ts- orshared.ts, holds the interface and any shared functionsserver/utils/cache/local.ts- thisStorageCacheAdapterserver/utils/cache/redis.tsserver/utils/cache/get.ts- not sure about this one, but just holds the maingetCacheAdapter. can't live in the shared file since that'd cause circular imports
generally we should have one class per file imo
|
looking good! i left a bunch of comments, mostly about code organisation though tbh |
app/composables/useAtproto.ts
Outdated
| const data = ref<PackageLikes | null>(null) | ||
| const error = ref<Error | null>(null) | ||
| const pending = ref(false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| const data = ref<PackageLikes | null>(null) | |
| const error = ref<Error | null>(null) | |
| const pending = ref(false) | |
| const data = shallowRef<PackageLikes | null>(null) | |
| const error = shallowRef<Error | null>(null) | |
| const pending = shallowRef(false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By the way, I dont think we need these as composables. We almost never want the data from the ref itself, but rather the data returned by mutate. Just use $fetch where we need it or capsule just the $fetch in a util (IMO)
app/pages/package/[...package].vue
Outdated
| server: false, | ||
| }) | ||
|
|
||
| const { mutate: likePackage } = useLikePackage(packageName.value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
passing packageName.value means, that when packageName updates the likePackage will like the initial value.
911bcd2 to
a8f9a02
Compare
|
Thank you everyone for all the feedback! I think I have addressed everything brought up. About to log off for a few hours and going come back to do some final testing and I have 2 todos I want to hit
|
|
it seems that likes aren't persisted, right? as in, when I navigate back to a package, it shows up as 'unliked' |
They should be 😬. Was some heavy refactoring today so may be a bit of a bug. Going give it some more heavy testing when I come back. Is that on the preview URL? Should be loaded into cache for a while and once the cache expire it grabs from constellation if you have liked the packaged and the total count |
6f240e2 to
1603f53
Compare
1603f53 to
f403b80
Compare
f403b80 to
934fd6d
Compare
|
I think I've taken troubleshooting about as far as I can on this side of the fence :/. I think someone with access to the Vercel service will need to look. All works locally both for upstash redis and useStorage When you like it updates the record in your user repo but does not show, notice after the 10 minutes it shows you liked it. Which is about the time it takes for cache entry to expire. I am passing in a fresh instance of Feels very much so like something in Vercel is caching web requests or that the cache backends may not be writing as expected. Any help would be greatly appreciated 🙏 |
Our first social feature 🥳 This closes #79
Adds
useStoragelocally and redis in prod with set expireCould be Improved
Concerns
I probably won't be able to address feedback till tomorrow around 22:00 UTC
Zen.mp4