mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
fix(stats): avoid scan fallback for public skill count
This commit is contained in:
+1
-1
@@ -87,7 +87,7 @@ bunx convex run --no-push devSeed:seedNixSkills
|
||||
bunx convex run --no-push devSeedExtra:seedExtraSkillsInternal
|
||||
|
||||
# Refresh the cached skills count (required after seeding)
|
||||
bunx convex run --no-push statsMaintenance:updateGlobalStatsInternal
|
||||
bunx convex run --no-push statsMaintenance:updateGlobalStatsAction
|
||||
```
|
||||
|
||||
To reset and re-seed:
|
||||
|
||||
@@ -52,18 +52,6 @@ export function isGlobalStatsStorageNotReadyError(error: unknown) {
|
||||
);
|
||||
}
|
||||
|
||||
export async function countPublicSkillsForGlobalStats(ctx: GlobalStatsReadCtx) {
|
||||
const digests = await ctx.db
|
||||
.query("skillSearchDigest")
|
||||
.withIndex("by_active_updated", (q) => q.eq("softDeletedAt", undefined))
|
||||
.collect();
|
||||
let count = 0;
|
||||
for (const digest of digests) {
|
||||
if (isPublicSkillDoc(digest)) count += 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
export async function setGlobalPublicSkillsCount(
|
||||
ctx: GlobalStatsWriteCtx,
|
||||
count: number,
|
||||
@@ -117,9 +105,8 @@ export async function adjustGlobalPublicSkillsCount(
|
||||
}
|
||||
|
||||
if (!existing) {
|
||||
// No baseline yet (e.g. fresh deploy). Initialize via full recount once.
|
||||
const count = await countPublicSkillsForGlobalStats(ctx);
|
||||
await setGlobalPublicSkillsCount(ctx, count, now);
|
||||
// No baseline yet. The paginated stats maintenance action reconciles the full count.
|
||||
await setGlobalPublicSkillsCount(ctx, Math.max(0, normalizedDelta), now);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,19 +15,6 @@ const countPublicSkillsHandler = (
|
||||
countPublicSkills as unknown as WrappedHandler<Record<string, never>, number>
|
||||
)._handler;
|
||||
|
||||
function makeSkillsQuery(
|
||||
skills: Array<{ softDeletedAt?: number; moderationStatus?: string | null }>,
|
||||
) {
|
||||
return {
|
||||
withIndex: (name: string) => {
|
||||
if (name !== "by_active_updated") throw new Error(`unexpected skills index ${name}`);
|
||||
return {
|
||||
collect: async () => skills,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("skills.countPublicSkills", () => {
|
||||
it("returns precomputed global stats count when available", async () => {
|
||||
const ctx = {
|
||||
@@ -40,9 +27,6 @@ describe("skills.countPublicSkills", () => {
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (table === "skillSearchDigest") {
|
||||
return makeSkillsQuery([]);
|
||||
}
|
||||
throw new Error(`unexpected table ${table}`);
|
||||
}),
|
||||
},
|
||||
@@ -52,7 +36,7 @@ describe("skills.countPublicSkills", () => {
|
||||
expect(result).toBe(123);
|
||||
});
|
||||
|
||||
it("falls back to live count when global stats row is missing", async () => {
|
||||
it("returns zero when the global stats row is missing", async () => {
|
||||
const ctx = {
|
||||
db: {
|
||||
query: vi.fn((table: string) => {
|
||||
@@ -63,41 +47,28 @@ describe("skills.countPublicSkills", () => {
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (table === "skillSearchDigest") {
|
||||
return makeSkillsQuery([
|
||||
{ softDeletedAt: undefined, moderationStatus: "active" },
|
||||
{ softDeletedAt: undefined, moderationStatus: "hidden" },
|
||||
{ softDeletedAt: undefined, moderationStatus: "active" },
|
||||
]);
|
||||
}
|
||||
throw new Error(`unexpected table ${table}`);
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const result = await countPublicSkillsHandler(ctx, {});
|
||||
expect(result).toBe(2);
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it("falls back to live count when globalStats table is unavailable", async () => {
|
||||
it("returns zero when globalStats table is unavailable", async () => {
|
||||
const ctx = {
|
||||
db: {
|
||||
query: vi.fn((table: string) => {
|
||||
if (table === "globalStats") {
|
||||
throw new Error("unexpected table globalStats");
|
||||
}
|
||||
if (table === "skillSearchDigest") {
|
||||
return makeSkillsQuery([
|
||||
{ softDeletedAt: undefined, moderationStatus: "active" },
|
||||
{ softDeletedAt: undefined, moderationStatus: "active" },
|
||||
]);
|
||||
}
|
||||
throw new Error(`unexpected table ${table}`);
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const result = await countPublicSkillsHandler(ctx, {});
|
||||
expect(result).toBe(2);
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
+1
-4
@@ -34,7 +34,6 @@ import {
|
||||
} from "./lib/githubIdentity";
|
||||
import {
|
||||
adjustGlobalPublicSkillsCount,
|
||||
countPublicSkillsForGlobalStats,
|
||||
getPublicSkillVisibilityDelta,
|
||||
isPublicSkillDoc,
|
||||
readGlobalPublicSkillsCount,
|
||||
@@ -3423,9 +3422,7 @@ export const countPublicSkills = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const statsCount = await readGlobalPublicSkillsCount(ctx);
|
||||
if (typeof statsCount === "number") return statsCount;
|
||||
// Fallback for uninitialized/missing globalStats storage.
|
||||
return countPublicSkillsForGlobalStats(ctx);
|
||||
return statsCount ?? 0;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -3,11 +3,7 @@ import { internal } from "./_generated/api";
|
||||
import type { Doc } from "./_generated/dataModel";
|
||||
import type { ActionCtx } from "./_generated/server";
|
||||
import { internalAction, internalMutation, internalQuery } from "./functions";
|
||||
import {
|
||||
countPublicSkillsForGlobalStats,
|
||||
isPublicSkillDoc,
|
||||
setGlobalPublicSkillsCount,
|
||||
} from "./lib/globalStats";
|
||||
import { isPublicSkillDoc, setGlobalPublicSkillsCount } from "./lib/globalStats";
|
||||
|
||||
const DEFAULT_BATCH_SIZE = 200;
|
||||
const MAX_BATCH_SIZE = 1000;
|
||||
@@ -189,8 +185,7 @@ function buildSkillStatPatch(skill: Doc<"skills">) {
|
||||
// nested `stats` object only for documents that pre-date the migration.
|
||||
const nextDownloads =
|
||||
typeof skill.statsDownloads === "number" ? skill.statsDownloads : stats.downloads;
|
||||
const nextStars =
|
||||
typeof skill.statsStars === "number" ? skill.statsStars : stats.stars;
|
||||
const nextStars = typeof skill.statsStars === "number" ? skill.statsStars : stats.stars;
|
||||
const nextInstallsCurrent =
|
||||
typeof skill.statsInstallsCurrent === "number"
|
||||
? skill.statsInstallsCurrent
|
||||
@@ -279,7 +274,9 @@ export async function reconcileSkillStarCountsHandler(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.withIndex("by_skill", (q: any) => q.eq("skillId", skill._id))
|
||||
.collect();
|
||||
const actualComments = commentRecords.filter((c: { softDeletedAt?: unknown }) => !c.softDeletedAt).length;
|
||||
const actualComments = commentRecords.filter(
|
||||
(c: { softDeletedAt?: unknown }) => !c.softDeletedAt,
|
||||
).length;
|
||||
|
||||
// Check if stats are out of sync (compare against the canonical value
|
||||
// used by toPublicSkill: prefer top-level field, fall back to nested).
|
||||
@@ -415,15 +412,3 @@ export const updateGlobalStatsAction = internalAction({
|
||||
return { count: total };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated Use updateGlobalStatsAction instead.
|
||||
* Kept as a manual emergency fallback only — do not re-add to crons.
|
||||
*/
|
||||
export const updateGlobalStatsInternal = internalMutation({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const count = await countPublicSkillsForGlobalStats(ctx);
|
||||
await setGlobalPublicSkillsCount(ctx, count);
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user