pilothub 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -0
- package/README.md +36 -129
- package/dist/browserAuth.d.ts +20 -0
- package/dist/browserAuth.js +156 -0
- package/dist/browserAuth.js.map +1 -0
- package/dist/browserAuth.test.d.ts +1 -0
- package/dist/browserAuth.test.js +83 -0
- package/dist/browserAuth.test.js.map +1 -0
- package/dist/cli/buildInfo.d.ts +3 -0
- package/dist/cli/buildInfo.js +103 -0
- package/dist/cli/buildInfo.js.map +1 -0
- package/dist/cli/commands/auth.d.ts +9 -0
- package/dist/cli/commands/auth.js +75 -0
- package/dist/cli/commands/auth.js.map +1 -0
- package/dist/cli/commands/delete.d.ts +11 -0
- package/dist/cli/commands/delete.js +67 -0
- package/dist/cli/commands/delete.js.map +1 -0
- package/dist/cli/commands/delete.test.d.ts +1 -0
- package/dist/cli/commands/delete.test.js +52 -0
- package/dist/cli/commands/delete.test.js.map +1 -0
- package/dist/cli/commands/publish.d.ts +9 -0
- package/dist/cli/commands/publish.js +87 -0
- package/dist/cli/commands/publish.js.map +1 -0
- package/dist/cli/commands/publish.test.d.ts +1 -0
- package/dist/cli/commands/publish.test.js +104 -0
- package/dist/cli/commands/publish.test.js.map +1 -0
- package/dist/cli/commands/skills.d.ts +23 -0
- package/dist/cli/commands/skills.js +298 -0
- package/dist/cli/commands/skills.js.map +1 -0
- package/dist/cli/commands/skills.test.d.ts +1 -0
- package/dist/cli/commands/skills.test.js +156 -0
- package/dist/cli/commands/skills.test.js.map +1 -0
- package/dist/cli/commands/star.d.ts +8 -0
- package/dist/cli/commands/star.js +38 -0
- package/dist/cli/commands/star.js.map +1 -0
- package/dist/cli/commands/sync.d.ts +3 -0
- package/dist/cli/commands/sync.js +160 -0
- package/dist/cli/commands/sync.js.map +1 -0
- package/dist/cli/commands/sync.test.d.ts +1 -0
- package/dist/cli/commands/sync.test.js +277 -0
- package/dist/cli/commands/sync.test.js.map +1 -0
- package/dist/cli/commands/syncHelpers.d.ts +76 -0
- package/dist/cli/commands/syncHelpers.js +349 -0
- package/dist/cli/commands/syncHelpers.js.map +1 -0
- package/dist/cli/commands/syncHelpers.test.d.ts +1 -0
- package/dist/cli/commands/syncHelpers.test.js +22 -0
- package/dist/cli/commands/syncHelpers.test.js.map +1 -0
- package/dist/cli/commands/syncTypes.d.ts +24 -0
- package/dist/cli/commands/syncTypes.js +2 -0
- package/dist/cli/commands/syncTypes.js.map +1 -0
- package/dist/cli/commands/unstar.d.ts +8 -0
- package/dist/cli/commands/unstar.js +38 -0
- package/dist/cli/commands/unstar.js.map +1 -0
- package/dist/cli/helpStyle.d.ts +13 -0
- package/dist/cli/helpStyle.js +38 -0
- package/dist/cli/helpStyle.js.map +1 -0
- package/dist/cli/pilotbotConfig.d.ts +6 -0
- package/dist/cli/pilotbotConfig.js +110 -0
- package/dist/cli/pilotbotConfig.js.map +1 -0
- package/dist/cli/pilotbotConfig.test.d.ts +1 -0
- package/dist/cli/pilotbotConfig.test.js +133 -0
- package/dist/cli/pilotbotConfig.test.js.map +1 -0
- package/dist/cli/registry.d.ts +7 -0
- package/dist/cli/registry.js +42 -0
- package/dist/cli/registry.js.map +1 -0
- package/dist/cli/registry.test.d.ts +1 -0
- package/dist/cli/registry.test.js +48 -0
- package/dist/cli/registry.test.js.map +1 -0
- package/dist/cli/scanSkills.d.ts +7 -0
- package/dist/cli/scanSkills.js +75 -0
- package/dist/cli/scanSkills.js.map +1 -0
- package/dist/cli/scanSkills.test.d.ts +1 -0
- package/dist/cli/scanSkills.test.js +60 -0
- package/dist/cli/scanSkills.test.js.map +1 -0
- package/dist/cli/slug.d.ts +2 -0
- package/dist/cli/slug.js +16 -0
- package/dist/cli/slug.js.map +1 -0
- package/dist/cli/types.d.ts +15 -0
- package/dist/cli/types.js +2 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/cli/ui.d.ts +7 -0
- package/dist/cli/ui.js +72 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +268 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +38 -0
- package/dist/config.js.map +1 -0
- package/dist/discovery.d.ts +5 -0
- package/dist/discovery.js +21 -0
- package/dist/discovery.js.map +1 -0
- package/dist/discovery.test.d.ts +1 -0
- package/dist/discovery.test.js +46 -0
- package/dist/discovery.test.js.map +1 -0
- package/dist/http.d.ts +32 -0
- package/dist/http.js +261 -0
- package/dist/http.js.map +1 -0
- package/dist/http.test.d.ts +1 -0
- package/dist/http.test.js +135 -0
- package/dist/http.test.js.map +1 -0
- package/dist/schema/ark.js.map +1 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/routes.js.map +1 -0
- package/{packages/schema/dist → dist/schema}/schemas.d.ts +0 -39
- package/{packages/schema/dist → dist/schema}/schemas.js +0 -22
- package/dist/schema/schemas.js.map +1 -0
- package/dist/schema/textFiles.js.map +1 -0
- package/dist/schema/textFiles.test.d.ts +1 -0
- package/dist/schema/textFiles.test.js +20 -0
- package/dist/schema/textFiles.test.js.map +1 -0
- package/dist/skills.d.ts +43 -0
- package/dist/skills.js +163 -0
- package/dist/skills.js.map +1 -0
- package/dist/skills.test.d.ts +1 -0
- package/dist/skills.test.js +144 -0
- package/dist/skills.test.js.map +1 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +27 -70
- package/.env.local.example +0 -19
- package/.github/workflows/ci.yml +0 -40
- package/.oxlintrc.json +0 -3
- package/AGENTS.md +0 -45
- package/CHANGELOG.md +0 -138
- package/DEPRECATIONS.md +0 -7
- package/biome.json +0 -41
- package/convex/_generated/api.d.ts +0 -153
- package/convex/_generated/api.js +0 -23
- package/convex/_generated/dataModel.d.ts +0 -60
- package/convex/_generated/server.d.ts +0 -143
- package/convex/_generated/server.js +0 -93
- package/convex/auth.config.ts +0 -8
- package/convex/auth.ts +0 -19
- package/convex/comments.ts +0 -88
- package/convex/crons.ts +0 -34
- package/convex/devSeed.ts +0 -459
- package/convex/devSeedExtra.ts +0 -541
- package/convex/downloads.ts +0 -78
- package/convex/githubBackups.ts +0 -170
- package/convex/githubBackupsNode.ts +0 -183
- package/convex/githubImport.ts +0 -317
- package/convex/githubSoulBackups.ts +0 -170
- package/convex/githubSoulBackupsNode.ts +0 -186
- package/convex/http.ts +0 -194
- package/convex/httpApi.handlers.test.ts +0 -488
- package/convex/httpApi.test.ts +0 -70
- package/convex/httpApi.ts +0 -305
- package/convex/httpApiV1.handlers.test.ts +0 -584
- package/convex/httpApiV1.ts +0 -1172
- package/convex/leaderboards.ts +0 -39
- package/convex/lib/access.ts +0 -36
- package/convex/lib/apiTokenAuth.ts +0 -36
- package/convex/lib/badges.ts +0 -50
- package/convex/lib/changelog.test.ts +0 -34
- package/convex/lib/changelog.ts +0 -278
- package/convex/lib/embeddings.ts +0 -38
- package/convex/lib/githubBackup.ts +0 -443
- package/convex/lib/githubImport.test.ts +0 -247
- package/convex/lib/githubImport.ts +0 -425
- package/convex/lib/githubSoulBackup.ts +0 -443
- package/convex/lib/leaderboards.ts +0 -103
- package/convex/lib/moderation.ts +0 -42
- package/convex/lib/public.ts +0 -89
- package/convex/lib/searchText.test.ts +0 -46
- package/convex/lib/searchText.ts +0 -27
- package/convex/lib/skillBackfill.test.ts +0 -34
- package/convex/lib/skillBackfill.ts +0 -67
- package/convex/lib/skillPublish.test.ts +0 -28
- package/convex/lib/skillPublish.ts +0 -284
- package/convex/lib/skillStats.ts +0 -80
- package/convex/lib/skills.test.ts +0 -197
- package/convex/lib/skills.ts +0 -273
- package/convex/lib/soulChangelog.ts +0 -273
- package/convex/lib/soulPublish.ts +0 -236
- package/convex/lib/tokens.test.ts +0 -33
- package/convex/lib/tokens.ts +0 -51
- package/convex/lib/webhooks.test.ts +0 -91
- package/convex/lib/webhooks.ts +0 -112
- package/convex/maintenance.test.ts +0 -270
- package/convex/maintenance.ts +0 -840
- package/convex/rateLimits.ts +0 -50
- package/convex/schema.ts +0 -472
- package/convex/search.test.ts +0 -12
- package/convex/search.ts +0 -254
- package/convex/seed.test.ts +0 -37
- package/convex/seed.ts +0 -254
- package/convex/seedSouls.ts +0 -111
- package/convex/skillStatEvents.ts +0 -568
- package/convex/skills.ts +0 -1606
- package/convex/soulComments.ts +0 -88
- package/convex/soulDownloads.ts +0 -14
- package/convex/soulStars.ts +0 -71
- package/convex/souls.ts +0 -570
- package/convex/stars.ts +0 -108
- package/convex/statsMaintenance.ts +0 -205
- package/convex/telemetry.ts +0 -434
- package/convex/tokens.ts +0 -88
- package/convex/tsconfig.json +0 -7
- package/convex/uploads.ts +0 -20
- package/convex/users.ts +0 -122
- package/convex/webhooks.ts +0 -50
- package/convex.json +0 -3
- package/docs/README.md +0 -32
- package/docs/api.md +0 -51
- package/docs/architecture.md +0 -61
- package/docs/auth.md +0 -54
- package/docs/cli.md +0 -117
- package/docs/deploy.md +0 -78
- package/docs/diffing.md +0 -84
- package/docs/github-import.md +0 -171
- package/docs/http-api.md +0 -187
- package/docs/manual-testing.md +0 -64
- package/docs/mintlify.md +0 -43
- package/docs/quickstart.md +0 -120
- package/docs/skill-format.md +0 -58
- package/docs/soul-format.md +0 -37
- package/docs/spec.md +0 -177
- package/docs/telemetry.md +0 -91
- package/docs/troubleshooting.md +0 -49
- package/docs/webhook.md +0 -51
- package/e2e/menu-smoke.pw.test.ts +0 -49
- package/e2e/pilothub.e2e.test.ts +0 -494
- package/e2e/search-exact.pw.test.ts +0 -97
- package/packages/pilothub/LICENSE +0 -22
- package/packages/pilothub/README.md +0 -57
- package/packages/pilothub/package.json +0 -41
- package/packages/pilothub/src/browserAuth.test.ts +0 -96
- package/packages/pilothub/src/browserAuth.ts +0 -174
- package/packages/pilothub/src/cli/buildInfo.ts +0 -94
- package/packages/pilothub/src/cli/commands/auth.ts +0 -97
- package/packages/pilothub/src/cli/commands/delete.test.ts +0 -73
- package/packages/pilothub/src/cli/commands/delete.ts +0 -83
- package/packages/pilothub/src/cli/commands/publish.test.ts +0 -122
- package/packages/pilothub/src/cli/commands/publish.ts +0 -108
- package/packages/pilothub/src/cli/commands/skills.test.ts +0 -191
- package/packages/pilothub/src/cli/commands/skills.ts +0 -380
- package/packages/pilothub/src/cli/commands/star.ts +0 -46
- package/packages/pilothub/src/cli/commands/sync.test.ts +0 -310
- package/packages/pilothub/src/cli/commands/sync.ts +0 -200
- package/packages/pilothub/src/cli/commands/syncHelpers.test.ts +0 -26
- package/packages/pilothub/src/cli/commands/syncHelpers.ts +0 -427
- package/packages/pilothub/src/cli/commands/syncTypes.ts +0 -27
- package/packages/pilothub/src/cli/commands/unstar.ts +0 -48
- package/packages/pilothub/src/cli/helpStyle.ts +0 -45
- package/packages/pilothub/src/cli/pilotbotConfig.test.ts +0 -159
- package/packages/pilothub/src/cli/pilotbotConfig.ts +0 -147
- package/packages/pilothub/src/cli/registry.test.ts +0 -63
- package/packages/pilothub/src/cli/registry.ts +0 -43
- package/packages/pilothub/src/cli/scanSkills.test.ts +0 -64
- package/packages/pilothub/src/cli/scanSkills.ts +0 -84
- package/packages/pilothub/src/cli/slug.ts +0 -16
- package/packages/pilothub/src/cli/types.ts +0 -12
- package/packages/pilothub/src/cli/ui.ts +0 -75
- package/packages/pilothub/src/cli.ts +0 -311
- package/packages/pilothub/src/config.ts +0 -36
- package/packages/pilothub/src/discovery.test.ts +0 -75
- package/packages/pilothub/src/discovery.ts +0 -19
- package/packages/pilothub/src/http.test.ts +0 -156
- package/packages/pilothub/src/http.ts +0 -301
- package/packages/pilothub/src/schema/ark.ts +0 -29
- package/packages/pilothub/src/schema/index.ts +0 -5
- package/packages/pilothub/src/schema/routes.ts +0 -22
- package/packages/pilothub/src/schema/schemas.ts +0 -260
- package/packages/pilothub/src/schema/textFiles.test.ts +0 -23
- package/packages/pilothub/src/schema/textFiles.ts +0 -66
- package/packages/pilothub/src/skills.test.ts +0 -191
- package/packages/pilothub/src/skills.ts +0 -172
- package/packages/pilothub/src/types.ts +0 -10
- package/packages/pilothub/tsconfig.json +0 -14
- package/packages/schema/README.md +0 -3
- package/packages/schema/dist/ark.js.map +0 -1
- package/packages/schema/dist/index.js.map +0 -1
- package/packages/schema/dist/routes.js.map +0 -1
- package/packages/schema/dist/schemas.js.map +0 -1
- package/packages/schema/dist/textFiles.js.map +0 -1
- package/packages/schema/package.json +0 -26
- package/packages/schema/src/ark.ts +0 -29
- package/packages/schema/src/index.ts +0 -5
- package/packages/schema/src/routes.ts +0 -22
- package/packages/schema/src/schemas.test.ts +0 -123
- package/packages/schema/src/schemas.ts +0 -287
- package/packages/schema/src/textFiles.test.ts +0 -23
- package/packages/schema/src/textFiles.ts +0 -66
- package/packages/schema/tsconfig.json +0 -15
- package/pilothub +0 -46
- package/playwright.config.ts +0 -33
- package/public/.well-known/pilothub.json +0 -6
- package/public/api/v1/openapi.json +0 -379
- package/public/favicon.ico +0 -0
- package/public/logo192.png +0 -0
- package/public/logo512.png +0 -0
- package/public/manifest.json +0 -25
- package/public/og.png +0 -0
- package/public/og.svg +0 -98
- package/public/pilot-logo.png +0 -0
- package/public/pilot-mark.png +0 -0
- package/public/robots.txt +0 -3
- package/public/tanstack-circle-logo.png +0 -0
- package/public/tanstack-word-logo-white.svg +0 -1
- package/scripts/check-peer-deps.ts +0 -56
- package/scripts/docs-list.ts +0 -148
- package/scripts/run-playwright-local.sh +0 -14
- package/server/og/fetchSkillOgMeta.ts +0 -27
- package/server/og/fetchSoulOgMeta.ts +0 -27
- package/server/og/ogAssets.ts +0 -80
- package/server/og/skillOgSvg.test.ts +0 -59
- package/server/og/skillOgSvg.ts +0 -258
- package/server/og/soulOgSvg.ts +0 -209
- package/server/routes/og/skill.png.ts +0 -103
- package/server/routes/og/soul.png.ts +0 -111
- package/src/__tests__/skill-detail-page.test.tsx +0 -86
- package/src/__tests__/skills-index.test.tsx +0 -145
- package/src/__tests__/upload.route.test.tsx +0 -228
- package/src/components/AppProviders.tsx +0 -19
- package/src/components/ClientOnly.tsx +0 -18
- package/src/components/Footer.tsx +0 -29
- package/src/components/Header.tsx +0 -295
- package/src/components/InstallSwitcher.tsx +0 -53
- package/src/components/SkillCard.tsx +0 -36
- package/src/components/SkillDetailPage.tsx +0 -817
- package/src/components/SkillDiffCard.tsx +0 -485
- package/src/components/SoulCard.tsx +0 -19
- package/src/components/SoulDetailPage.tsx +0 -263
- package/src/components/UserBootstrap.tsx +0 -18
- package/src/components/ui/dropdown-menu.tsx +0 -67
- package/src/components/ui/toggle-group.tsx +0 -35
- package/src/convex/client.ts +0 -3
- package/src/lib/badges.ts +0 -29
- package/src/lib/diffing.test.ts +0 -163
- package/src/lib/diffing.ts +0 -106
- package/src/lib/gravatar.test.ts +0 -9
- package/src/lib/gravatar.ts +0 -158
- package/src/lib/og.test.ts +0 -142
- package/src/lib/og.ts +0 -156
- package/src/lib/publicUser.ts +0 -39
- package/src/lib/roles.ts +0 -19
- package/src/lib/site.test.ts +0 -130
- package/src/lib/site.ts +0 -84
- package/src/lib/theme-transition.test.ts +0 -134
- package/src/lib/theme-transition.ts +0 -134
- package/src/lib/theme.test.tsx +0 -88
- package/src/lib/theme.ts +0 -43
- package/src/lib/uploadFiles.jsdom.test.ts +0 -33
- package/src/lib/uploadFiles.test.ts +0 -123
- package/src/lib/uploadFiles.ts +0 -245
- package/src/lib/uploadUtils.test.ts +0 -78
- package/src/lib/uploadUtils.ts +0 -93
- package/src/lib/useAuthStatus.ts +0 -12
- package/src/lib/utils.test.ts +0 -9
- package/src/lib/utils.ts +0 -6
- package/src/logo.svg +0 -12
- package/src/routeTree.gen.ts +0 -345
- package/src/router.tsx +0 -17
- package/src/routes/$owner/$slug.tsx +0 -55
- package/src/routes/__root.tsx +0 -136
- package/src/routes/admin.tsx +0 -11
- package/src/routes/cli/auth.tsx +0 -168
- package/src/routes/dashboard.tsx +0 -97
- package/src/routes/import.tsx +0 -415
- package/src/routes/index.tsx +0 -252
- package/src/routes/management.tsx +0 -529
- package/src/routes/settings.tsx +0 -203
- package/src/routes/skills/index.tsx +0 -422
- package/src/routes/souls/$slug.tsx +0 -55
- package/src/routes/souls/index.tsx +0 -243
- package/src/routes/stars.tsx +0 -68
- package/src/routes/u/$handle.tsx +0 -307
- package/src/routes/upload/utils.ts +0 -81
- package/src/routes/upload.tsx +0 -499
- package/src/styles.css +0 -2718
- package/tsconfig.json +0 -24
- package/tsconfig.oxlint.json +0 -16
- package/vercel.json +0 -8
- package/vite.config.ts +0 -48
- package/vitest.config.ts +0 -47
- package/vitest.e2e.config.ts +0 -11
- package/vitest.setup.ts +0 -1
- /package/{packages/pilothub/bin → bin}/pilothub.js +0 -0
- /package/{packages/schema/dist → dist/schema}/ark.d.ts +0 -0
- /package/{packages/schema/dist → dist/schema}/ark.js +0 -0
- /package/{packages/schema/dist → dist/schema}/index.d.ts +0 -0
- /package/{packages/schema/dist → dist/schema}/index.js +0 -0
- /package/{packages/schema/dist → dist/schema}/routes.d.ts +0 -0
- /package/{packages/schema/dist → dist/schema}/routes.js +0 -0
- /package/{packages/schema/dist → dist/schema}/textFiles.d.ts +0 -0
- /package/{packages/schema/dist → dist/schema}/textFiles.js +0 -0
|
@@ -1,568 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Skill Stat Events - Event-sourced stats processing for skills
|
|
3
|
-
*
|
|
4
|
-
* Instead of updating skill stats synchronously in the hot path (which can cause
|
|
5
|
-
* contention when multiple users download/star/install the same skill), we insert
|
|
6
|
-
* lightweight event records and process them in batches via a cron job.
|
|
7
|
-
*
|
|
8
|
-
* Flow:
|
|
9
|
-
* 1. User action (download, star, install) → insertStatEvent() writes to skillStatEvents table
|
|
10
|
-
* 2. Cron job runs every 5 minutes → processSkillStatEventsInternal() processes batches
|
|
11
|
-
* 3. Events are aggregated per-skill to minimize database operations
|
|
12
|
-
* 4. Stats are applied to skill documents and daily stats tables
|
|
13
|
-
* 5. Events are marked as processed (kept forever for auditing)
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { v } from 'convex/values'
|
|
17
|
-
import { internal } from './_generated/api'
|
|
18
|
-
import type { Doc, Id } from './_generated/dataModel'
|
|
19
|
-
import type { MutationCtx } from './_generated/server'
|
|
20
|
-
import { internalAction, internalMutation, internalQuery } from './_generated/server'
|
|
21
|
-
import { applySkillStatDeltas, bumpDailySkillStats } from './lib/skillStats'
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Event types that affect skill stats:
|
|
25
|
-
*
|
|
26
|
-
* - download: User downloaded skill as zip (+1 downloads)
|
|
27
|
-
* - star: User starred the skill (+1 stars)
|
|
28
|
-
* - unstar: User removed their star (-1 stars)
|
|
29
|
-
* - install_new: First time this user installed this skill (+1 installsAllTime, +1 installsCurrent)
|
|
30
|
-
* - install_reactivate: User re-added skill after removing it (+1 installsCurrent only)
|
|
31
|
-
* - install_deactivate: User removed skill from all projects (-1 installsCurrent)
|
|
32
|
-
* - install_clear: User cleared all telemetry data (custom delta for both allTime and current)
|
|
33
|
-
*/
|
|
34
|
-
export type StatEventKind =
|
|
35
|
-
| 'download'
|
|
36
|
-
| 'star'
|
|
37
|
-
| 'unstar'
|
|
38
|
-
| 'install_new'
|
|
39
|
-
| 'install_reactivate'
|
|
40
|
-
| 'install_deactivate'
|
|
41
|
-
| 'install_clear'
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Insert a stat event to be processed later by the cron job.
|
|
45
|
-
*
|
|
46
|
-
* This is called from the hot path (downloads, stars, telemetry) instead of
|
|
47
|
-
* directly updating skill stats. It's a single insert with no read-modify-write
|
|
48
|
-
* cycle, so it's fast and doesn't contend with other operations on the same skill.
|
|
49
|
-
*
|
|
50
|
-
* @param ctx - Mutation context
|
|
51
|
-
* @param params.skillId - The skill being affected
|
|
52
|
-
* @param params.kind - Type of event (download, star, install_new, etc.)
|
|
53
|
-
* @param params.occurredAt - When the event happened (defaults to now). Important for
|
|
54
|
-
* daily stats bucketing - we want downloads at 11:55 PM Monday
|
|
55
|
-
* to count toward Monday's stats even if processed on Tuesday.
|
|
56
|
-
* @param params.delta - Only used for install_clear events, specifies exact delta amounts
|
|
57
|
-
*/
|
|
58
|
-
export async function insertStatEvent(
|
|
59
|
-
ctx: MutationCtx,
|
|
60
|
-
params: {
|
|
61
|
-
skillId: Id<'skills'>
|
|
62
|
-
kind: StatEventKind
|
|
63
|
-
occurredAt?: number
|
|
64
|
-
delta?: { allTime: number; current: number }
|
|
65
|
-
},
|
|
66
|
-
) {
|
|
67
|
-
await ctx.db.insert('skillStatEvents', {
|
|
68
|
-
skillId: params.skillId,
|
|
69
|
-
kind: params.kind,
|
|
70
|
-
delta: params.delta,
|
|
71
|
-
occurredAt: params.occurredAt ?? Date.now(),
|
|
72
|
-
processedAt: undefined,
|
|
73
|
-
})
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Aggregated deltas for a single skill after processing multiple events.
|
|
78
|
-
*
|
|
79
|
-
* When we process a batch of 100 events, many might be for the same skill.
|
|
80
|
-
* Instead of updating the skill document once per event, we aggregate all
|
|
81
|
-
* events for each skill and apply a single update.
|
|
82
|
-
*
|
|
83
|
-
* The downloadEvents and installNewEvents arrays store the original timestamps
|
|
84
|
-
* so we can update daily stats with the correct day bucket for each event.
|
|
85
|
-
*/
|
|
86
|
-
type AggregatedDeltas = {
|
|
87
|
-
downloads: number
|
|
88
|
-
stars: number
|
|
89
|
-
installsAllTime: number
|
|
90
|
-
installsCurrent: number
|
|
91
|
-
/** Original timestamps for each download event (for daily stats bucketing) */
|
|
92
|
-
downloadEvents: number[]
|
|
93
|
-
/** Original timestamps for each new install event (for daily stats bucketing) */
|
|
94
|
-
installNewEvents: number[]
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Aggregate multiple events for a single skill into net deltas.
|
|
99
|
-
*
|
|
100
|
-
* Example: If a skill has these events in the batch:
|
|
101
|
-
* - download (Mon 11pm)
|
|
102
|
-
* - download (Tue 1am)
|
|
103
|
-
* - star
|
|
104
|
-
* - unstar
|
|
105
|
-
* - star
|
|
106
|
-
*
|
|
107
|
-
* The result would be:
|
|
108
|
-
* - downloads: 2
|
|
109
|
-
* - stars: 1 (net: +1 -1 +1 = +1)
|
|
110
|
-
* - downloadEvents: [<Mon 11pm timestamp>, <Tue 1am timestamp>]
|
|
111
|
-
*
|
|
112
|
-
* This aggregation reduces the number of database operations from N events
|
|
113
|
-
* to 1 skill update + N daily stat updates (which themselves may coalesce
|
|
114
|
-
* if multiple events fall on the same day).
|
|
115
|
-
*/
|
|
116
|
-
function aggregateEvents(events: Doc<'skillStatEvents'>[]): AggregatedDeltas {
|
|
117
|
-
const result: AggregatedDeltas = {
|
|
118
|
-
downloads: 0,
|
|
119
|
-
stars: 0,
|
|
120
|
-
installsAllTime: 0,
|
|
121
|
-
installsCurrent: 0,
|
|
122
|
-
downloadEvents: [],
|
|
123
|
-
installNewEvents: [],
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
for (const event of events) {
|
|
127
|
-
switch (event.kind) {
|
|
128
|
-
case 'download':
|
|
129
|
-
result.downloads += 1
|
|
130
|
-
result.downloadEvents.push(event.occurredAt)
|
|
131
|
-
break
|
|
132
|
-
case 'star':
|
|
133
|
-
result.stars += 1
|
|
134
|
-
break
|
|
135
|
-
case 'unstar':
|
|
136
|
-
result.stars -= 1
|
|
137
|
-
break
|
|
138
|
-
case 'install_new':
|
|
139
|
-
// New user installing for the first time: count toward both lifetime and current
|
|
140
|
-
result.installsAllTime += 1
|
|
141
|
-
result.installsCurrent += 1
|
|
142
|
-
result.installNewEvents.push(event.occurredAt)
|
|
143
|
-
break
|
|
144
|
-
case 'install_reactivate':
|
|
145
|
-
// User re-added skill after removing: only affects current count
|
|
146
|
-
result.installsCurrent += 1
|
|
147
|
-
break
|
|
148
|
-
case 'install_deactivate':
|
|
149
|
-
// User removed skill from all projects: only affects current count
|
|
150
|
-
result.installsCurrent -= 1
|
|
151
|
-
break
|
|
152
|
-
case 'install_clear':
|
|
153
|
-
// User cleared telemetry: uses custom delta values (typically negative)
|
|
154
|
-
if (event.delta) {
|
|
155
|
-
result.installsAllTime += event.delta.allTime
|
|
156
|
-
result.installsCurrent += event.delta.current
|
|
157
|
-
}
|
|
158
|
-
break
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return result
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Process a batch of unprocessed stat events.
|
|
167
|
-
*
|
|
168
|
-
* Called by cron every 5 minutes. Processes up to batchSize events (default 100).
|
|
169
|
-
* If the batch is full, schedules an immediate follow-up run to drain the queue.
|
|
170
|
-
*
|
|
171
|
-
* Processing steps:
|
|
172
|
-
* 1. Query unprocessed events (processedAt is undefined)
|
|
173
|
-
* 2. Group events by skillId to minimize skill document fetches
|
|
174
|
-
* 3. For each skill:
|
|
175
|
-
* a. Fetch the skill document once
|
|
176
|
-
* b. Aggregate all events for this skill into net deltas
|
|
177
|
-
* c. Apply deltas to skill stats (downloads, stars, installs)
|
|
178
|
-
* d. Update daily stats for trending (using original event timestamps)
|
|
179
|
-
* e. Mark all events as processed
|
|
180
|
-
* 4. If batch was full, schedule another run immediately
|
|
181
|
-
*
|
|
182
|
-
* Aggregation levels:
|
|
183
|
-
* - Level 1: Batch of 100 events from the queue
|
|
184
|
-
* - Level 2: Group by skillId (e.g., 100 events → 30 unique skills)
|
|
185
|
-
* - Level 3: Aggregate events per skill (e.g., 5 events → 1 skill update)
|
|
186
|
-
* - Level 4: Daily stats may coalesce (e.g., 3 downloads same day → 1 upsert)
|
|
187
|
-
*/
|
|
188
|
-
export const processSkillStatEventsInternal = internalMutation({
|
|
189
|
-
args: { batchSize: v.optional(v.number()) },
|
|
190
|
-
handler: async (ctx, args) => {
|
|
191
|
-
const batchSize = args.batchSize ?? 100
|
|
192
|
-
const now = Date.now()
|
|
193
|
-
|
|
194
|
-
// Level 1: Fetch a batch of unprocessed events
|
|
195
|
-
const events = await ctx.db
|
|
196
|
-
.query('skillStatEvents')
|
|
197
|
-
.withIndex('by_unprocessed', (q) => q.eq('processedAt', undefined))
|
|
198
|
-
.take(batchSize)
|
|
199
|
-
|
|
200
|
-
if (events.length === 0) {
|
|
201
|
-
return { processed: 0 }
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Level 2: Group events by skillId to minimize database reads
|
|
205
|
-
// Instead of fetching the same skill document multiple times,
|
|
206
|
-
// we fetch it once and process all its events together
|
|
207
|
-
const eventsBySkill = new Map<Id<'skills'>, Doc<'skillStatEvents'>[]>()
|
|
208
|
-
for (const event of events) {
|
|
209
|
-
const existing = eventsBySkill.get(event.skillId) ?? []
|
|
210
|
-
existing.push(event)
|
|
211
|
-
eventsBySkill.set(event.skillId, existing)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Process each skill's events
|
|
215
|
-
for (const [skillId, skillEvents] of eventsBySkill) {
|
|
216
|
-
const skill = await ctx.db.get(skillId)
|
|
217
|
-
|
|
218
|
-
// Skill was deleted - just mark events as processed
|
|
219
|
-
if (!skill) {
|
|
220
|
-
for (const event of skillEvents) {
|
|
221
|
-
await ctx.db.patch(event._id, { processedAt: now })
|
|
222
|
-
}
|
|
223
|
-
continue
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Level 3: Aggregate all events for this skill into net deltas
|
|
227
|
-
// e.g., 3 downloads + 2 stars - 1 unstar → { downloads: 3, stars: 1 }
|
|
228
|
-
const deltas = aggregateEvents(skillEvents)
|
|
229
|
-
|
|
230
|
-
// Apply aggregated deltas to skill stats (single update per skill)
|
|
231
|
-
if (
|
|
232
|
-
deltas.downloads !== 0 ||
|
|
233
|
-
deltas.stars !== 0 ||
|
|
234
|
-
deltas.installsAllTime !== 0 ||
|
|
235
|
-
deltas.installsCurrent !== 0
|
|
236
|
-
) {
|
|
237
|
-
const patch = applySkillStatDeltas(skill, {
|
|
238
|
-
downloads: deltas.downloads,
|
|
239
|
-
stars: deltas.stars,
|
|
240
|
-
installsAllTime: deltas.installsAllTime,
|
|
241
|
-
installsCurrent: deltas.installsCurrent,
|
|
242
|
-
})
|
|
243
|
-
await ctx.db.patch(skill._id, {
|
|
244
|
-
...patch,
|
|
245
|
-
updatedAt: now,
|
|
246
|
-
})
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Update daily stats for trending/leaderboards
|
|
250
|
-
// We use the ORIGINAL event timestamp (occurredAt) so that:
|
|
251
|
-
// - A download at Mon 11:55 PM counts toward Monday's stats
|
|
252
|
-
// - Even if the cron processes it on Tuesday
|
|
253
|
-
//
|
|
254
|
-
// Level 4: bumpDailySkillStats does its own coalescing - multiple
|
|
255
|
-
// events on the same day will update the same daily record
|
|
256
|
-
for (const occurredAt of deltas.downloadEvents) {
|
|
257
|
-
await bumpDailySkillStats(ctx, { skillId, now: occurredAt, downloads: 1 })
|
|
258
|
-
}
|
|
259
|
-
for (const occurredAt of deltas.installNewEvents) {
|
|
260
|
-
await bumpDailySkillStats(ctx, { skillId, now: occurredAt, installs: 1 })
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Mark all events for this skill as processed
|
|
264
|
-
for (const event of skillEvents) {
|
|
265
|
-
await ctx.db.patch(event._id, { processedAt: now })
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// If we hit the batch limit, there may be more events waiting.
|
|
270
|
-
// Schedule an immediate follow-up run to drain the queue.
|
|
271
|
-
// This ensures high-volume periods don't create a backlog.
|
|
272
|
-
if (events.length === batchSize) {
|
|
273
|
-
await ctx.scheduler.runAfter(0, internal.skillStatEvents.processSkillStatEventsInternal, {
|
|
274
|
-
batchSize,
|
|
275
|
-
})
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
return { processed: events.length }
|
|
279
|
-
},
|
|
280
|
-
})
|
|
281
|
-
|
|
282
|
-
// ============================================================================
|
|
283
|
-
// Action-based processing (cursor-based, runs outside transaction window)
|
|
284
|
-
// ============================================================================
|
|
285
|
-
|
|
286
|
-
const CURSOR_KEY = 'skill_stat_events'
|
|
287
|
-
const EVENT_BATCH_SIZE = 500
|
|
288
|
-
const MAX_SKILLS_PER_RUN = 500
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Fetch a batch of events after the given cursor (by _creationTime).
|
|
292
|
-
* Returns events sorted by _creationTime ascending.
|
|
293
|
-
*/
|
|
294
|
-
export const getUnprocessedEventBatch = internalQuery({
|
|
295
|
-
args: {
|
|
296
|
-
cursorCreationTime: v.optional(v.number()),
|
|
297
|
-
limit: v.optional(v.number()),
|
|
298
|
-
},
|
|
299
|
-
handler: async (ctx, args) => {
|
|
300
|
-
const limit = args.limit ?? EVENT_BATCH_SIZE
|
|
301
|
-
const cursor = args.cursorCreationTime
|
|
302
|
-
|
|
303
|
-
// Query events after the cursor using the built-in creation time index
|
|
304
|
-
const events = await ctx.db
|
|
305
|
-
.query('skillStatEvents')
|
|
306
|
-
.withIndex('by_creation_time', (q) =>
|
|
307
|
-
cursor !== undefined ? q.gt('_creationTime', cursor) : q,
|
|
308
|
-
)
|
|
309
|
-
.take(limit)
|
|
310
|
-
return events
|
|
311
|
-
},
|
|
312
|
-
})
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Get the current cursor position from the cursors table.
|
|
316
|
-
*/
|
|
317
|
-
export const getStatEventCursor = internalQuery({
|
|
318
|
-
args: {},
|
|
319
|
-
handler: async (ctx) => {
|
|
320
|
-
const cursor = await ctx.db
|
|
321
|
-
.query('skillStatUpdateCursors')
|
|
322
|
-
.withIndex('by_key', (q) => q.eq('key', CURSOR_KEY))
|
|
323
|
-
.unique()
|
|
324
|
-
return cursor?.cursorCreationTime
|
|
325
|
-
},
|
|
326
|
-
})
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* Validator for skill deltas passed to the mutation.
|
|
330
|
-
*/
|
|
331
|
-
const skillDeltaValidator = v.object({
|
|
332
|
-
skillId: v.id('skills'),
|
|
333
|
-
downloads: v.number(),
|
|
334
|
-
stars: v.number(),
|
|
335
|
-
installsAllTime: v.number(),
|
|
336
|
-
installsCurrent: v.number(),
|
|
337
|
-
downloadEvents: v.array(v.number()),
|
|
338
|
-
installNewEvents: v.array(v.number()),
|
|
339
|
-
})
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* Apply aggregated stats to skills and update the cursor.
|
|
343
|
-
* This is a single atomic mutation that:
|
|
344
|
-
* 1. Updates all affected skills with their aggregated deltas
|
|
345
|
-
* 2. Updates daily stats for trending
|
|
346
|
-
* 3. Advances the cursor to the new position
|
|
347
|
-
*/
|
|
348
|
-
export const applyAggregatedStatsAndUpdateCursor = internalMutation({
|
|
349
|
-
args: {
|
|
350
|
-
skillDeltas: v.array(skillDeltaValidator),
|
|
351
|
-
newCursor: v.number(),
|
|
352
|
-
},
|
|
353
|
-
handler: async (ctx, args) => {
|
|
354
|
-
const now = Date.now()
|
|
355
|
-
|
|
356
|
-
// Process each skill's aggregated deltas
|
|
357
|
-
for (const delta of args.skillDeltas) {
|
|
358
|
-
const skill = await ctx.db.get(delta.skillId)
|
|
359
|
-
|
|
360
|
-
// Skill was deleted - skip
|
|
361
|
-
if (!skill) {
|
|
362
|
-
continue
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Apply aggregated deltas to skill stats
|
|
366
|
-
if (
|
|
367
|
-
delta.downloads !== 0 ||
|
|
368
|
-
delta.stars !== 0 ||
|
|
369
|
-
delta.installsAllTime !== 0 ||
|
|
370
|
-
delta.installsCurrent !== 0
|
|
371
|
-
) {
|
|
372
|
-
const patch = applySkillStatDeltas(skill, {
|
|
373
|
-
downloads: delta.downloads,
|
|
374
|
-
stars: delta.stars,
|
|
375
|
-
installsAllTime: delta.installsAllTime,
|
|
376
|
-
installsCurrent: delta.installsCurrent,
|
|
377
|
-
})
|
|
378
|
-
await ctx.db.patch(skill._id, {
|
|
379
|
-
...patch,
|
|
380
|
-
updatedAt: now,
|
|
381
|
-
})
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// Update daily stats for trending/leaderboards
|
|
385
|
-
for (const occurredAt of delta.downloadEvents) {
|
|
386
|
-
await bumpDailySkillStats(ctx, { skillId: delta.skillId, now: occurredAt, downloads: 1 })
|
|
387
|
-
}
|
|
388
|
-
for (const occurredAt of delta.installNewEvents) {
|
|
389
|
-
await bumpDailySkillStats(ctx, { skillId: delta.skillId, now: occurredAt, installs: 1 })
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// Update cursor position (upsert)
|
|
394
|
-
const existingCursor = await ctx.db
|
|
395
|
-
.query('skillStatUpdateCursors')
|
|
396
|
-
.withIndex('by_key', (q) => q.eq('key', CURSOR_KEY))
|
|
397
|
-
.unique()
|
|
398
|
-
|
|
399
|
-
if (existingCursor) {
|
|
400
|
-
await ctx.db.patch(existingCursor._id, {
|
|
401
|
-
cursorCreationTime: args.newCursor,
|
|
402
|
-
updatedAt: now,
|
|
403
|
-
})
|
|
404
|
-
} else {
|
|
405
|
-
await ctx.db.insert('skillStatUpdateCursors', {
|
|
406
|
-
key: CURSOR_KEY,
|
|
407
|
-
cursorCreationTime: args.newCursor,
|
|
408
|
-
updatedAt: now,
|
|
409
|
-
})
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
return { skillsUpdated: args.skillDeltas.length }
|
|
413
|
-
},
|
|
414
|
-
})
|
|
415
|
-
|
|
416
|
-
/**
|
|
417
|
-
* Action that processes skill stat events in batches outside the transaction window.
|
|
418
|
-
*
|
|
419
|
-
* Algorithm:
|
|
420
|
-
* 1. Get current cursor position
|
|
421
|
-
* 2. Fetch events in batches of 500, aggregating as we go
|
|
422
|
-
* 3. Stop when we have >= 500 unique skills OR run out of events
|
|
423
|
-
* 4. Call mutation to apply all deltas and update cursor atomically
|
|
424
|
-
* 5. Self-schedule if we stopped due to skill limit (not exhaustion)
|
|
425
|
-
*/
|
|
426
|
-
export const processSkillStatEventsAction = internalAction({
|
|
427
|
-
args: {},
|
|
428
|
-
handler: async (ctx) => {
|
|
429
|
-
// Get current cursor position (convert null to undefined for consistency)
|
|
430
|
-
const cursorResult = await ctx.runQuery(internal.skillStatEvents.getStatEventCursor)
|
|
431
|
-
let cursor: number | undefined = cursorResult ?? undefined
|
|
432
|
-
|
|
433
|
-
console.log(`[STAT-AGG] Starting aggregation, cursor=${cursor ?? 'none'}`)
|
|
434
|
-
|
|
435
|
-
// Aggregated deltas per skill
|
|
436
|
-
const aggregatedBySkill = new Map<
|
|
437
|
-
Id<'skills'>,
|
|
438
|
-
{
|
|
439
|
-
downloads: number
|
|
440
|
-
stars: number
|
|
441
|
-
installsAllTime: number
|
|
442
|
-
installsCurrent: number
|
|
443
|
-
downloadEvents: number[]
|
|
444
|
-
installNewEvents: number[]
|
|
445
|
-
}
|
|
446
|
-
>()
|
|
447
|
-
|
|
448
|
-
let maxCreationTime: number | undefined = cursor
|
|
449
|
-
let exhausted = false
|
|
450
|
-
let totalEventsFetched = 0
|
|
451
|
-
|
|
452
|
-
// Fetch and aggregate until we have enough skills or run out of events
|
|
453
|
-
while (aggregatedBySkill.size < MAX_SKILLS_PER_RUN) {
|
|
454
|
-
const events = await ctx.runQuery(internal.skillStatEvents.getUnprocessedEventBatch, {
|
|
455
|
-
cursorCreationTime: cursor,
|
|
456
|
-
limit: EVENT_BATCH_SIZE,
|
|
457
|
-
})
|
|
458
|
-
|
|
459
|
-
if (events.length === 0) {
|
|
460
|
-
exhausted = true
|
|
461
|
-
break
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
totalEventsFetched += events.length
|
|
465
|
-
const skillsBefore = aggregatedBySkill.size
|
|
466
|
-
|
|
467
|
-
// Aggregate events into per-skill deltas
|
|
468
|
-
for (const event of events) {
|
|
469
|
-
let skillDelta = aggregatedBySkill.get(event.skillId)
|
|
470
|
-
if (!skillDelta) {
|
|
471
|
-
skillDelta = {
|
|
472
|
-
downloads: 0,
|
|
473
|
-
stars: 0,
|
|
474
|
-
installsAllTime: 0,
|
|
475
|
-
installsCurrent: 0,
|
|
476
|
-
downloadEvents: [],
|
|
477
|
-
installNewEvents: [],
|
|
478
|
-
}
|
|
479
|
-
aggregatedBySkill.set(event.skillId, skillDelta)
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// Apply event to aggregated deltas
|
|
483
|
-
switch (event.kind) {
|
|
484
|
-
case 'download':
|
|
485
|
-
skillDelta.downloads += 1
|
|
486
|
-
skillDelta.downloadEvents.push(event.occurredAt)
|
|
487
|
-
break
|
|
488
|
-
case 'star':
|
|
489
|
-
skillDelta.stars += 1
|
|
490
|
-
break
|
|
491
|
-
case 'unstar':
|
|
492
|
-
skillDelta.stars -= 1
|
|
493
|
-
break
|
|
494
|
-
case 'install_new':
|
|
495
|
-
skillDelta.installsAllTime += 1
|
|
496
|
-
skillDelta.installsCurrent += 1
|
|
497
|
-
skillDelta.installNewEvents.push(event.occurredAt)
|
|
498
|
-
break
|
|
499
|
-
case 'install_reactivate':
|
|
500
|
-
skillDelta.installsCurrent += 1
|
|
501
|
-
break
|
|
502
|
-
case 'install_deactivate':
|
|
503
|
-
skillDelta.installsCurrent -= 1
|
|
504
|
-
break
|
|
505
|
-
case 'install_clear':
|
|
506
|
-
if (event.delta) {
|
|
507
|
-
skillDelta.installsAllTime += event.delta.allTime
|
|
508
|
-
skillDelta.installsCurrent += event.delta.current
|
|
509
|
-
}
|
|
510
|
-
break
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
// Track highest _creationTime seen
|
|
514
|
-
if (maxCreationTime === undefined || event._creationTime > maxCreationTime) {
|
|
515
|
-
maxCreationTime = event._creationTime
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// Update cursor for next batch fetch
|
|
520
|
-
cursor = events[events.length - 1]._creationTime
|
|
521
|
-
|
|
522
|
-
console.log(
|
|
523
|
-
`[STAT-AGG] Fetched ${events.length} events, ${aggregatedBySkill.size - skillsBefore} new skills (${aggregatedBySkill.size} total)`,
|
|
524
|
-
)
|
|
525
|
-
|
|
526
|
-
// If we got fewer than requested, we've exhausted the events
|
|
527
|
-
if (events.length < EVENT_BATCH_SIZE) {
|
|
528
|
-
exhausted = true
|
|
529
|
-
break
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// If we have nothing to process, we're done
|
|
534
|
-
if (aggregatedBySkill.size === 0 || maxCreationTime === undefined) {
|
|
535
|
-
console.log('[STAT-AGG] No events to process, done')
|
|
536
|
-
return { processed: 0, skillsUpdated: 0, exhausted: true }
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// Convert map to array for mutation
|
|
540
|
-
const skillDeltas = Array.from(aggregatedBySkill.entries()).map(([skillId, delta]) => ({
|
|
541
|
-
skillId,
|
|
542
|
-
...delta,
|
|
543
|
-
}))
|
|
544
|
-
|
|
545
|
-
console.log(
|
|
546
|
-
`[STAT-AGG] Running mutation for ${skillDeltas.length} skills (${totalEventsFetched} total events)`,
|
|
547
|
-
)
|
|
548
|
-
|
|
549
|
-
// Apply all deltas and update cursor atomically
|
|
550
|
-
await ctx.runMutation(internal.skillStatEvents.applyAggregatedStatsAndUpdateCursor, {
|
|
551
|
-
skillDeltas,
|
|
552
|
-
newCursor: maxCreationTime,
|
|
553
|
-
})
|
|
554
|
-
|
|
555
|
-
// Self-schedule if we stopped because of skill limit, not exhaustion
|
|
556
|
-
if (!exhausted) {
|
|
557
|
-
console.log('[STAT-AGG] More events remaining, self-scheduling')
|
|
558
|
-
await ctx.scheduler.runAfter(0, internal.skillStatEvents.processSkillStatEventsAction, {})
|
|
559
|
-
} else {
|
|
560
|
-
console.log('[STAT-AGG] All events processed, done')
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
return {
|
|
564
|
-
skillsUpdated: skillDeltas.length,
|
|
565
|
-
exhausted,
|
|
566
|
-
}
|
|
567
|
-
},
|
|
568
|
-
})
|