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.
Files changed (388) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +36 -129
  3. package/dist/browserAuth.d.ts +20 -0
  4. package/dist/browserAuth.js +156 -0
  5. package/dist/browserAuth.js.map +1 -0
  6. package/dist/browserAuth.test.d.ts +1 -0
  7. package/dist/browserAuth.test.js +83 -0
  8. package/dist/browserAuth.test.js.map +1 -0
  9. package/dist/cli/buildInfo.d.ts +3 -0
  10. package/dist/cli/buildInfo.js +103 -0
  11. package/dist/cli/buildInfo.js.map +1 -0
  12. package/dist/cli/commands/auth.d.ts +9 -0
  13. package/dist/cli/commands/auth.js +75 -0
  14. package/dist/cli/commands/auth.js.map +1 -0
  15. package/dist/cli/commands/delete.d.ts +11 -0
  16. package/dist/cli/commands/delete.js +67 -0
  17. package/dist/cli/commands/delete.js.map +1 -0
  18. package/dist/cli/commands/delete.test.d.ts +1 -0
  19. package/dist/cli/commands/delete.test.js +52 -0
  20. package/dist/cli/commands/delete.test.js.map +1 -0
  21. package/dist/cli/commands/publish.d.ts +9 -0
  22. package/dist/cli/commands/publish.js +87 -0
  23. package/dist/cli/commands/publish.js.map +1 -0
  24. package/dist/cli/commands/publish.test.d.ts +1 -0
  25. package/dist/cli/commands/publish.test.js +104 -0
  26. package/dist/cli/commands/publish.test.js.map +1 -0
  27. package/dist/cli/commands/skills.d.ts +23 -0
  28. package/dist/cli/commands/skills.js +298 -0
  29. package/dist/cli/commands/skills.js.map +1 -0
  30. package/dist/cli/commands/skills.test.d.ts +1 -0
  31. package/dist/cli/commands/skills.test.js +156 -0
  32. package/dist/cli/commands/skills.test.js.map +1 -0
  33. package/dist/cli/commands/star.d.ts +8 -0
  34. package/dist/cli/commands/star.js +38 -0
  35. package/dist/cli/commands/star.js.map +1 -0
  36. package/dist/cli/commands/sync.d.ts +3 -0
  37. package/dist/cli/commands/sync.js +160 -0
  38. package/dist/cli/commands/sync.js.map +1 -0
  39. package/dist/cli/commands/sync.test.d.ts +1 -0
  40. package/dist/cli/commands/sync.test.js +277 -0
  41. package/dist/cli/commands/sync.test.js.map +1 -0
  42. package/dist/cli/commands/syncHelpers.d.ts +76 -0
  43. package/dist/cli/commands/syncHelpers.js +349 -0
  44. package/dist/cli/commands/syncHelpers.js.map +1 -0
  45. package/dist/cli/commands/syncHelpers.test.d.ts +1 -0
  46. package/dist/cli/commands/syncHelpers.test.js +22 -0
  47. package/dist/cli/commands/syncHelpers.test.js.map +1 -0
  48. package/dist/cli/commands/syncTypes.d.ts +24 -0
  49. package/dist/cli/commands/syncTypes.js +2 -0
  50. package/dist/cli/commands/syncTypes.js.map +1 -0
  51. package/dist/cli/commands/unstar.d.ts +8 -0
  52. package/dist/cli/commands/unstar.js +38 -0
  53. package/dist/cli/commands/unstar.js.map +1 -0
  54. package/dist/cli/helpStyle.d.ts +13 -0
  55. package/dist/cli/helpStyle.js +38 -0
  56. package/dist/cli/helpStyle.js.map +1 -0
  57. package/dist/cli/pilotbotConfig.d.ts +6 -0
  58. package/dist/cli/pilotbotConfig.js +110 -0
  59. package/dist/cli/pilotbotConfig.js.map +1 -0
  60. package/dist/cli/pilotbotConfig.test.d.ts +1 -0
  61. package/dist/cli/pilotbotConfig.test.js +133 -0
  62. package/dist/cli/pilotbotConfig.test.js.map +1 -0
  63. package/dist/cli/registry.d.ts +7 -0
  64. package/dist/cli/registry.js +42 -0
  65. package/dist/cli/registry.js.map +1 -0
  66. package/dist/cli/registry.test.d.ts +1 -0
  67. package/dist/cli/registry.test.js +48 -0
  68. package/dist/cli/registry.test.js.map +1 -0
  69. package/dist/cli/scanSkills.d.ts +7 -0
  70. package/dist/cli/scanSkills.js +75 -0
  71. package/dist/cli/scanSkills.js.map +1 -0
  72. package/dist/cli/scanSkills.test.d.ts +1 -0
  73. package/dist/cli/scanSkills.test.js +60 -0
  74. package/dist/cli/scanSkills.test.js.map +1 -0
  75. package/dist/cli/slug.d.ts +2 -0
  76. package/dist/cli/slug.js +16 -0
  77. package/dist/cli/slug.js.map +1 -0
  78. package/dist/cli/types.d.ts +15 -0
  79. package/dist/cli/types.js +2 -0
  80. package/dist/cli/types.js.map +1 -0
  81. package/dist/cli/ui.d.ts +7 -0
  82. package/dist/cli/ui.js +72 -0
  83. package/dist/cli/ui.js.map +1 -0
  84. package/dist/cli.d.ts +2 -0
  85. package/dist/cli.js +268 -0
  86. package/dist/cli.js.map +1 -0
  87. package/dist/config.d.ts +4 -0
  88. package/dist/config.js +38 -0
  89. package/dist/config.js.map +1 -0
  90. package/dist/discovery.d.ts +5 -0
  91. package/dist/discovery.js +21 -0
  92. package/dist/discovery.js.map +1 -0
  93. package/dist/discovery.test.d.ts +1 -0
  94. package/dist/discovery.test.js +46 -0
  95. package/dist/discovery.test.js.map +1 -0
  96. package/dist/http.d.ts +32 -0
  97. package/dist/http.js +261 -0
  98. package/dist/http.js.map +1 -0
  99. package/dist/http.test.d.ts +1 -0
  100. package/dist/http.test.js +135 -0
  101. package/dist/http.test.js.map +1 -0
  102. package/dist/schema/ark.js.map +1 -0
  103. package/dist/schema/index.js.map +1 -0
  104. package/dist/schema/routes.js.map +1 -0
  105. package/{packages/schema/dist → dist/schema}/schemas.d.ts +0 -39
  106. package/{packages/schema/dist → dist/schema}/schemas.js +0 -22
  107. package/dist/schema/schemas.js.map +1 -0
  108. package/dist/schema/textFiles.js.map +1 -0
  109. package/dist/schema/textFiles.test.d.ts +1 -0
  110. package/dist/schema/textFiles.test.js +20 -0
  111. package/dist/schema/textFiles.test.js.map +1 -0
  112. package/dist/skills.d.ts +43 -0
  113. package/dist/skills.js +163 -0
  114. package/dist/skills.js.map +1 -0
  115. package/dist/skills.test.d.ts +1 -0
  116. package/dist/skills.test.js +144 -0
  117. package/dist/skills.test.js.map +1 -0
  118. package/dist/types.d.ts +7 -0
  119. package/dist/types.js +2 -0
  120. package/dist/types.js.map +1 -0
  121. package/package.json +27 -70
  122. package/.env.local.example +0 -19
  123. package/.github/workflows/ci.yml +0 -40
  124. package/.oxlintrc.json +0 -3
  125. package/AGENTS.md +0 -45
  126. package/CHANGELOG.md +0 -138
  127. package/DEPRECATIONS.md +0 -7
  128. package/biome.json +0 -41
  129. package/convex/_generated/api.d.ts +0 -153
  130. package/convex/_generated/api.js +0 -23
  131. package/convex/_generated/dataModel.d.ts +0 -60
  132. package/convex/_generated/server.d.ts +0 -143
  133. package/convex/_generated/server.js +0 -93
  134. package/convex/auth.config.ts +0 -8
  135. package/convex/auth.ts +0 -19
  136. package/convex/comments.ts +0 -88
  137. package/convex/crons.ts +0 -34
  138. package/convex/devSeed.ts +0 -459
  139. package/convex/devSeedExtra.ts +0 -541
  140. package/convex/downloads.ts +0 -78
  141. package/convex/githubBackups.ts +0 -170
  142. package/convex/githubBackupsNode.ts +0 -183
  143. package/convex/githubImport.ts +0 -317
  144. package/convex/githubSoulBackups.ts +0 -170
  145. package/convex/githubSoulBackupsNode.ts +0 -186
  146. package/convex/http.ts +0 -194
  147. package/convex/httpApi.handlers.test.ts +0 -488
  148. package/convex/httpApi.test.ts +0 -70
  149. package/convex/httpApi.ts +0 -305
  150. package/convex/httpApiV1.handlers.test.ts +0 -584
  151. package/convex/httpApiV1.ts +0 -1172
  152. package/convex/leaderboards.ts +0 -39
  153. package/convex/lib/access.ts +0 -36
  154. package/convex/lib/apiTokenAuth.ts +0 -36
  155. package/convex/lib/badges.ts +0 -50
  156. package/convex/lib/changelog.test.ts +0 -34
  157. package/convex/lib/changelog.ts +0 -278
  158. package/convex/lib/embeddings.ts +0 -38
  159. package/convex/lib/githubBackup.ts +0 -443
  160. package/convex/lib/githubImport.test.ts +0 -247
  161. package/convex/lib/githubImport.ts +0 -425
  162. package/convex/lib/githubSoulBackup.ts +0 -443
  163. package/convex/lib/leaderboards.ts +0 -103
  164. package/convex/lib/moderation.ts +0 -42
  165. package/convex/lib/public.ts +0 -89
  166. package/convex/lib/searchText.test.ts +0 -46
  167. package/convex/lib/searchText.ts +0 -27
  168. package/convex/lib/skillBackfill.test.ts +0 -34
  169. package/convex/lib/skillBackfill.ts +0 -67
  170. package/convex/lib/skillPublish.test.ts +0 -28
  171. package/convex/lib/skillPublish.ts +0 -284
  172. package/convex/lib/skillStats.ts +0 -80
  173. package/convex/lib/skills.test.ts +0 -197
  174. package/convex/lib/skills.ts +0 -273
  175. package/convex/lib/soulChangelog.ts +0 -273
  176. package/convex/lib/soulPublish.ts +0 -236
  177. package/convex/lib/tokens.test.ts +0 -33
  178. package/convex/lib/tokens.ts +0 -51
  179. package/convex/lib/webhooks.test.ts +0 -91
  180. package/convex/lib/webhooks.ts +0 -112
  181. package/convex/maintenance.test.ts +0 -270
  182. package/convex/maintenance.ts +0 -840
  183. package/convex/rateLimits.ts +0 -50
  184. package/convex/schema.ts +0 -472
  185. package/convex/search.test.ts +0 -12
  186. package/convex/search.ts +0 -254
  187. package/convex/seed.test.ts +0 -37
  188. package/convex/seed.ts +0 -254
  189. package/convex/seedSouls.ts +0 -111
  190. package/convex/skillStatEvents.ts +0 -568
  191. package/convex/skills.ts +0 -1606
  192. package/convex/soulComments.ts +0 -88
  193. package/convex/soulDownloads.ts +0 -14
  194. package/convex/soulStars.ts +0 -71
  195. package/convex/souls.ts +0 -570
  196. package/convex/stars.ts +0 -108
  197. package/convex/statsMaintenance.ts +0 -205
  198. package/convex/telemetry.ts +0 -434
  199. package/convex/tokens.ts +0 -88
  200. package/convex/tsconfig.json +0 -7
  201. package/convex/uploads.ts +0 -20
  202. package/convex/users.ts +0 -122
  203. package/convex/webhooks.ts +0 -50
  204. package/convex.json +0 -3
  205. package/docs/README.md +0 -32
  206. package/docs/api.md +0 -51
  207. package/docs/architecture.md +0 -61
  208. package/docs/auth.md +0 -54
  209. package/docs/cli.md +0 -117
  210. package/docs/deploy.md +0 -78
  211. package/docs/diffing.md +0 -84
  212. package/docs/github-import.md +0 -171
  213. package/docs/http-api.md +0 -187
  214. package/docs/manual-testing.md +0 -64
  215. package/docs/mintlify.md +0 -43
  216. package/docs/quickstart.md +0 -120
  217. package/docs/skill-format.md +0 -58
  218. package/docs/soul-format.md +0 -37
  219. package/docs/spec.md +0 -177
  220. package/docs/telemetry.md +0 -91
  221. package/docs/troubleshooting.md +0 -49
  222. package/docs/webhook.md +0 -51
  223. package/e2e/menu-smoke.pw.test.ts +0 -49
  224. package/e2e/pilothub.e2e.test.ts +0 -494
  225. package/e2e/search-exact.pw.test.ts +0 -97
  226. package/packages/pilothub/LICENSE +0 -22
  227. package/packages/pilothub/README.md +0 -57
  228. package/packages/pilothub/package.json +0 -41
  229. package/packages/pilothub/src/browserAuth.test.ts +0 -96
  230. package/packages/pilothub/src/browserAuth.ts +0 -174
  231. package/packages/pilothub/src/cli/buildInfo.ts +0 -94
  232. package/packages/pilothub/src/cli/commands/auth.ts +0 -97
  233. package/packages/pilothub/src/cli/commands/delete.test.ts +0 -73
  234. package/packages/pilothub/src/cli/commands/delete.ts +0 -83
  235. package/packages/pilothub/src/cli/commands/publish.test.ts +0 -122
  236. package/packages/pilothub/src/cli/commands/publish.ts +0 -108
  237. package/packages/pilothub/src/cli/commands/skills.test.ts +0 -191
  238. package/packages/pilothub/src/cli/commands/skills.ts +0 -380
  239. package/packages/pilothub/src/cli/commands/star.ts +0 -46
  240. package/packages/pilothub/src/cli/commands/sync.test.ts +0 -310
  241. package/packages/pilothub/src/cli/commands/sync.ts +0 -200
  242. package/packages/pilothub/src/cli/commands/syncHelpers.test.ts +0 -26
  243. package/packages/pilothub/src/cli/commands/syncHelpers.ts +0 -427
  244. package/packages/pilothub/src/cli/commands/syncTypes.ts +0 -27
  245. package/packages/pilothub/src/cli/commands/unstar.ts +0 -48
  246. package/packages/pilothub/src/cli/helpStyle.ts +0 -45
  247. package/packages/pilothub/src/cli/pilotbotConfig.test.ts +0 -159
  248. package/packages/pilothub/src/cli/pilotbotConfig.ts +0 -147
  249. package/packages/pilothub/src/cli/registry.test.ts +0 -63
  250. package/packages/pilothub/src/cli/registry.ts +0 -43
  251. package/packages/pilothub/src/cli/scanSkills.test.ts +0 -64
  252. package/packages/pilothub/src/cli/scanSkills.ts +0 -84
  253. package/packages/pilothub/src/cli/slug.ts +0 -16
  254. package/packages/pilothub/src/cli/types.ts +0 -12
  255. package/packages/pilothub/src/cli/ui.ts +0 -75
  256. package/packages/pilothub/src/cli.ts +0 -311
  257. package/packages/pilothub/src/config.ts +0 -36
  258. package/packages/pilothub/src/discovery.test.ts +0 -75
  259. package/packages/pilothub/src/discovery.ts +0 -19
  260. package/packages/pilothub/src/http.test.ts +0 -156
  261. package/packages/pilothub/src/http.ts +0 -301
  262. package/packages/pilothub/src/schema/ark.ts +0 -29
  263. package/packages/pilothub/src/schema/index.ts +0 -5
  264. package/packages/pilothub/src/schema/routes.ts +0 -22
  265. package/packages/pilothub/src/schema/schemas.ts +0 -260
  266. package/packages/pilothub/src/schema/textFiles.test.ts +0 -23
  267. package/packages/pilothub/src/schema/textFiles.ts +0 -66
  268. package/packages/pilothub/src/skills.test.ts +0 -191
  269. package/packages/pilothub/src/skills.ts +0 -172
  270. package/packages/pilothub/src/types.ts +0 -10
  271. package/packages/pilothub/tsconfig.json +0 -14
  272. package/packages/schema/README.md +0 -3
  273. package/packages/schema/dist/ark.js.map +0 -1
  274. package/packages/schema/dist/index.js.map +0 -1
  275. package/packages/schema/dist/routes.js.map +0 -1
  276. package/packages/schema/dist/schemas.js.map +0 -1
  277. package/packages/schema/dist/textFiles.js.map +0 -1
  278. package/packages/schema/package.json +0 -26
  279. package/packages/schema/src/ark.ts +0 -29
  280. package/packages/schema/src/index.ts +0 -5
  281. package/packages/schema/src/routes.ts +0 -22
  282. package/packages/schema/src/schemas.test.ts +0 -123
  283. package/packages/schema/src/schemas.ts +0 -287
  284. package/packages/schema/src/textFiles.test.ts +0 -23
  285. package/packages/schema/src/textFiles.ts +0 -66
  286. package/packages/schema/tsconfig.json +0 -15
  287. package/pilothub +0 -46
  288. package/playwright.config.ts +0 -33
  289. package/public/.well-known/pilothub.json +0 -6
  290. package/public/api/v1/openapi.json +0 -379
  291. package/public/favicon.ico +0 -0
  292. package/public/logo192.png +0 -0
  293. package/public/logo512.png +0 -0
  294. package/public/manifest.json +0 -25
  295. package/public/og.png +0 -0
  296. package/public/og.svg +0 -98
  297. package/public/pilot-logo.png +0 -0
  298. package/public/pilot-mark.png +0 -0
  299. package/public/robots.txt +0 -3
  300. package/public/tanstack-circle-logo.png +0 -0
  301. package/public/tanstack-word-logo-white.svg +0 -1
  302. package/scripts/check-peer-deps.ts +0 -56
  303. package/scripts/docs-list.ts +0 -148
  304. package/scripts/run-playwright-local.sh +0 -14
  305. package/server/og/fetchSkillOgMeta.ts +0 -27
  306. package/server/og/fetchSoulOgMeta.ts +0 -27
  307. package/server/og/ogAssets.ts +0 -80
  308. package/server/og/skillOgSvg.test.ts +0 -59
  309. package/server/og/skillOgSvg.ts +0 -258
  310. package/server/og/soulOgSvg.ts +0 -209
  311. package/server/routes/og/skill.png.ts +0 -103
  312. package/server/routes/og/soul.png.ts +0 -111
  313. package/src/__tests__/skill-detail-page.test.tsx +0 -86
  314. package/src/__tests__/skills-index.test.tsx +0 -145
  315. package/src/__tests__/upload.route.test.tsx +0 -228
  316. package/src/components/AppProviders.tsx +0 -19
  317. package/src/components/ClientOnly.tsx +0 -18
  318. package/src/components/Footer.tsx +0 -29
  319. package/src/components/Header.tsx +0 -295
  320. package/src/components/InstallSwitcher.tsx +0 -53
  321. package/src/components/SkillCard.tsx +0 -36
  322. package/src/components/SkillDetailPage.tsx +0 -817
  323. package/src/components/SkillDiffCard.tsx +0 -485
  324. package/src/components/SoulCard.tsx +0 -19
  325. package/src/components/SoulDetailPage.tsx +0 -263
  326. package/src/components/UserBootstrap.tsx +0 -18
  327. package/src/components/ui/dropdown-menu.tsx +0 -67
  328. package/src/components/ui/toggle-group.tsx +0 -35
  329. package/src/convex/client.ts +0 -3
  330. package/src/lib/badges.ts +0 -29
  331. package/src/lib/diffing.test.ts +0 -163
  332. package/src/lib/diffing.ts +0 -106
  333. package/src/lib/gravatar.test.ts +0 -9
  334. package/src/lib/gravatar.ts +0 -158
  335. package/src/lib/og.test.ts +0 -142
  336. package/src/lib/og.ts +0 -156
  337. package/src/lib/publicUser.ts +0 -39
  338. package/src/lib/roles.ts +0 -19
  339. package/src/lib/site.test.ts +0 -130
  340. package/src/lib/site.ts +0 -84
  341. package/src/lib/theme-transition.test.ts +0 -134
  342. package/src/lib/theme-transition.ts +0 -134
  343. package/src/lib/theme.test.tsx +0 -88
  344. package/src/lib/theme.ts +0 -43
  345. package/src/lib/uploadFiles.jsdom.test.ts +0 -33
  346. package/src/lib/uploadFiles.test.ts +0 -123
  347. package/src/lib/uploadFiles.ts +0 -245
  348. package/src/lib/uploadUtils.test.ts +0 -78
  349. package/src/lib/uploadUtils.ts +0 -93
  350. package/src/lib/useAuthStatus.ts +0 -12
  351. package/src/lib/utils.test.ts +0 -9
  352. package/src/lib/utils.ts +0 -6
  353. package/src/logo.svg +0 -12
  354. package/src/routeTree.gen.ts +0 -345
  355. package/src/router.tsx +0 -17
  356. package/src/routes/$owner/$slug.tsx +0 -55
  357. package/src/routes/__root.tsx +0 -136
  358. package/src/routes/admin.tsx +0 -11
  359. package/src/routes/cli/auth.tsx +0 -168
  360. package/src/routes/dashboard.tsx +0 -97
  361. package/src/routes/import.tsx +0 -415
  362. package/src/routes/index.tsx +0 -252
  363. package/src/routes/management.tsx +0 -529
  364. package/src/routes/settings.tsx +0 -203
  365. package/src/routes/skills/index.tsx +0 -422
  366. package/src/routes/souls/$slug.tsx +0 -55
  367. package/src/routes/souls/index.tsx +0 -243
  368. package/src/routes/stars.tsx +0 -68
  369. package/src/routes/u/$handle.tsx +0 -307
  370. package/src/routes/upload/utils.ts +0 -81
  371. package/src/routes/upload.tsx +0 -499
  372. package/src/styles.css +0 -2718
  373. package/tsconfig.json +0 -24
  374. package/tsconfig.oxlint.json +0 -16
  375. package/vercel.json +0 -8
  376. package/vite.config.ts +0 -48
  377. package/vitest.config.ts +0 -47
  378. package/vitest.e2e.config.ts +0 -11
  379. package/vitest.setup.ts +0 -1
  380. /package/{packages/pilothub/bin → bin}/pilothub.js +0 -0
  381. /package/{packages/schema/dist → dist/schema}/ark.d.ts +0 -0
  382. /package/{packages/schema/dist → dist/schema}/ark.js +0 -0
  383. /package/{packages/schema/dist → dist/schema}/index.d.ts +0 -0
  384. /package/{packages/schema/dist → dist/schema}/index.js +0 -0
  385. /package/{packages/schema/dist → dist/schema}/routes.d.ts +0 -0
  386. /package/{packages/schema/dist → dist/schema}/routes.js +0 -0
  387. /package/{packages/schema/dist → dist/schema}/textFiles.d.ts +0 -0
  388. /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
- })