@atproto/ozone 0.1.173 → 0.1.174
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/CHANGELOG.md +9 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +40 -0
- package/dist/api/index.js.map +1 -1
- package/dist/api/moderation/emitEvent.d.ts.map +1 -1
- package/dist/api/moderation/emitEvent.js +31 -0
- package/dist/api/moderation/emitEvent.js.map +1 -1
- package/dist/api/queue/assignModerator.d.ts +4 -0
- package/dist/api/queue/assignModerator.d.ts.map +1 -0
- package/dist/api/queue/assignModerator.js +28 -0
- package/dist/api/queue/assignModerator.js.map +1 -0
- package/dist/api/queue/createQueue.d.ts +4 -0
- package/dist/api/queue/createQueue.d.ts.map +1 -0
- package/dist/api/queue/createQueue.js +44 -0
- package/dist/api/queue/createQueue.js.map +1 -0
- package/dist/api/queue/deleteQueue.d.ts +4 -0
- package/dist/api/queue/deleteQueue.d.ts.map +1 -0
- package/dist/api/queue/deleteQueue.js +40 -0
- package/dist/api/queue/deleteQueue.js.map +1 -0
- package/dist/api/queue/getAssignments.d.ts +4 -0
- package/dist/api/queue/getAssignments.d.ts.map +1 -0
- package/dist/api/queue/getAssignments.js +19 -0
- package/dist/api/queue/getAssignments.js.map +1 -0
- package/dist/api/queue/listQueues.d.ts +4 -0
- package/dist/api/queue/listQueues.d.ts.map +1 -0
- package/dist/api/queue/listQueues.js +29 -0
- package/dist/api/queue/listQueues.js.map +1 -0
- package/dist/api/queue/routeReports.d.ts +4 -0
- package/dist/api/queue/routeReports.d.ts.map +1 -0
- package/dist/api/queue/routeReports.js +33 -0
- package/dist/api/queue/routeReports.js.map +1 -0
- package/dist/api/queue/unassignModerator.d.ts +4 -0
- package/dist/api/queue/unassignModerator.d.ts.map +1 -0
- package/dist/api/queue/unassignModerator.js +24 -0
- package/dist/api/queue/unassignModerator.js.map +1 -0
- package/dist/api/queue/updateQueue.d.ts +4 -0
- package/dist/api/queue/updateQueue.d.ts.map +1 -0
- package/dist/api/queue/updateQueue.js +39 -0
- package/dist/api/queue/updateQueue.js.map +1 -0
- package/dist/api/report/assignModerator.d.ts +4 -0
- package/dist/api/report/assignModerator.d.ts.map +1 -0
- package/dist/api/report/assignModerator.js +33 -0
- package/dist/api/report/assignModerator.js.map +1 -0
- package/dist/api/report/createActivity.d.ts +4 -0
- package/dist/api/report/createActivity.d.ts.map +1 -0
- package/dist/api/report/createActivity.js +44 -0
- package/dist/api/report/createActivity.js.map +1 -0
- package/dist/api/report/getAssignments.d.ts +4 -0
- package/dist/api/report/getAssignments.d.ts.map +1 -0
- package/dist/api/report/getAssignments.js +19 -0
- package/dist/api/report/getAssignments.js.map +1 -0
- package/dist/api/report/getHistoricalStats.d.ts +4 -0
- package/dist/api/report/getHistoricalStats.d.ts.map +1 -0
- package/dist/api/report/getHistoricalStats.js +32 -0
- package/dist/api/report/getHistoricalStats.js.map +1 -0
- package/dist/api/report/getLatestReport.d.ts +4 -0
- package/dist/api/report/getLatestReport.d.ts.map +1 -0
- package/dist/api/report/getLatestReport.js +31 -0
- package/dist/api/report/getLatestReport.js.map +1 -0
- package/dist/api/report/getLiveStats.d.ts +4 -0
- package/dist/api/report/getLiveStats.d.ts.map +1 -0
- package/dist/api/report/getLiveStats.js +25 -0
- package/dist/api/report/getLiveStats.js.map +1 -0
- package/dist/api/report/getReport.d.ts +4 -0
- package/dist/api/report/getReport.d.ts.map +1 -0
- package/dist/api/report/getReport.js +35 -0
- package/dist/api/report/getReport.js.map +1 -0
- package/dist/api/report/listActivities.d.ts +4 -0
- package/dist/api/report/listActivities.d.ts.map +1 -0
- package/dist/api/report/listActivities.js +25 -0
- package/dist/api/report/listActivities.js.map +1 -0
- package/dist/api/report/queryReports.d.ts +4 -0
- package/dist/api/report/queryReports.d.ts.map +1 -0
- package/dist/api/report/queryReports.js +29 -0
- package/dist/api/report/queryReports.js.map +1 -0
- package/dist/api/report/reassignQueue.d.ts +4 -0
- package/dist/api/report/reassignQueue.d.ts.map +1 -0
- package/dist/api/report/reassignQueue.js +45 -0
- package/dist/api/report/reassignQueue.js.map +1 -0
- package/dist/api/report/refreshStats.d.ts +4 -0
- package/dist/api/report/refreshStats.d.ts.map +1 -0
- package/dist/api/report/refreshStats.js +26 -0
- package/dist/api/report/refreshStats.js.map +1 -0
- package/dist/api/report/unassignModerator.d.ts +4 -0
- package/dist/api/report/unassignModerator.d.ts.map +1 -0
- package/dist/api/report/unassignModerator.js +21 -0
- package/dist/api/report/unassignModerator.js.map +1 -0
- package/dist/api/util.d.ts +2 -0
- package/dist/api/util.d.ts.map +1 -1
- package/dist/api/util.js +9 -1
- package/dist/api/util.js.map +1 -1
- package/dist/assignment/index.d.ts +89 -0
- package/dist/assignment/index.d.ts.map +1 -0
- package/dist/assignment/index.js +537 -0
- package/dist/assignment/index.js.map +1 -0
- package/dist/config/config.d.ts +14 -0
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +9 -0
- package/dist/config/config.js.map +1 -1
- package/dist/config/env.d.ts +3 -0
- package/dist/config/env.d.ts.map +1 -1
- package/dist/config/env.js +3 -0
- package/dist/config/env.js.map +1 -1
- package/dist/context.d.ts +9 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +31 -10
- package/dist/context.js.map +1 -1
- package/dist/daemon/context.d.ts +6 -0
- package/dist/daemon/context.d.ts.map +1 -1
- package/dist/daemon/context.js +28 -4
- package/dist/daemon/context.js.map +1 -1
- package/dist/daemon/job-cursor.d.ts +5 -0
- package/dist/daemon/job-cursor.d.ts.map +1 -0
- package/dist/daemon/job-cursor.js +28 -0
- package/dist/daemon/job-cursor.js.map +1 -0
- package/dist/daemon/queue-router.d.ts +17 -0
- package/dist/daemon/queue-router.d.ts.map +1 -0
- package/dist/daemon/queue-router.js +114 -0
- package/dist/daemon/queue-router.js.map +1 -0
- package/dist/daemon/stats-computer.d.ts +51 -0
- package/dist/daemon/stats-computer.d.ts.map +1 -0
- package/dist/daemon/stats-computer.js +117 -0
- package/dist/daemon/stats-computer.js.map +1 -0
- package/dist/daemon/strike-expiry-processor.d.ts.map +1 -1
- package/dist/daemon/strike-expiry-processor.js +4 -19
- package/dist/daemon/strike-expiry-processor.js.map +1 -1
- package/dist/db/migrations/20260219T164523000Z-create-report-table.d.ts +4 -0
- package/dist/db/migrations/20260219T164523000Z-create-report-table.d.ts.map +1 -0
- package/dist/db/migrations/20260219T164523000Z-create-report-table.js +126 -0
- package/dist/db/migrations/20260219T164523000Z-create-report-table.js.map +1 -0
- package/dist/db/migrations/20260219T165302248Z-moderator-assignment.d.ts +4 -0
- package/dist/db/migrations/20260219T165302248Z-moderator-assignment.d.ts.map +1 -0
- package/dist/db/migrations/20260219T165302248Z-moderator-assignment.js +35 -0
- package/dist/db/migrations/20260219T165302248Z-moderator-assignment.js.map +1 -0
- package/dist/db/migrations/20260225T000000000Z-add-report-queue-table.d.ts +4 -0
- package/dist/db/migrations/20260225T000000000Z-add-report-queue-table.d.ts.map +1 -0
- package/dist/db/migrations/20260225T000000000Z-add-report-queue-table.js +36 -0
- package/dist/db/migrations/20260225T000000000Z-add-report-queue-table.js.map +1 -0
- package/dist/db/migrations/20260313T000000000Z-add-report-activity-table.d.ts +4 -0
- package/dist/db/migrations/20260313T000000000Z-add-report-activity-table.d.ts.map +1 -0
- package/dist/db/migrations/20260313T000000000Z-add-report-activity-table.js +39 -0
- package/dist/db/migrations/20260313T000000000Z-add-report-activity-table.js.map +1 -0
- package/dist/db/migrations/20260318T152058935Z-add-report-stat.d.ts +4 -0
- package/dist/db/migrations/20260318T152058935Z-add-report-stat.d.ts.map +1 -0
- package/dist/db/migrations/20260318T152058935Z-add-report-stat.js +34 -0
- package/dist/db/migrations/20260318T152058935Z-add-report-stat.js.map +1 -0
- package/dist/db/migrations/index.d.ts +5 -0
- package/dist/db/migrations/index.d.ts.map +1 -1
- package/dist/db/migrations/index.js +6 -1
- package/dist/db/migrations/index.js.map +1 -1
- package/dist/db/pagination.d.ts +31 -0
- package/dist/db/pagination.d.ts.map +1 -1
- package/dist/db/pagination.js +74 -1
- package/dist/db/pagination.js.map +1 -1
- package/dist/db/schema/index.d.ts +6 -1
- package/dist/db/schema/index.d.ts.map +1 -1
- package/dist/db/schema/index.js.map +1 -1
- package/dist/db/schema/moderator_assignment.d.ts +14 -0
- package/dist/db/schema/moderator_assignment.d.ts.map +1 -0
- package/dist/db/schema/moderator_assignment.js +5 -0
- package/dist/db/schema/moderator_assignment.js.map +1 -0
- package/dist/db/schema/report.d.ts +25 -0
- package/dist/db/schema/report.d.ts.map +1 -0
- package/dist/db/schema/report.js +5 -0
- package/dist/db/schema/report.js.map +1 -0
- package/dist/db/schema/report_activity.d.ts +18 -0
- package/dist/db/schema/report_activity.d.ts.map +1 -0
- package/dist/db/schema/report_activity.js +5 -0
- package/dist/db/schema/report_activity.js.map +1 -0
- package/dist/db/schema/report_queue.d.ts +19 -0
- package/dist/db/schema/report_queue.d.ts.map +1 -0
- package/dist/db/schema/report_queue.js +5 -0
- package/dist/db/schema/report_queue.js.map +1 -0
- package/dist/db/schema/report_stat.d.ts +20 -0
- package/dist/db/schema/report_stat.d.ts.map +1 -0
- package/dist/db/schema/report_stat.js +5 -0
- package/dist/db/schema/report_stat.js.map +1 -0
- package/dist/lexicon/index.d.ts +50 -0
- package/dist/lexicon/index.d.ts.map +1 -1
- package/dist/lexicon/index.js +120 -2
- package/dist/lexicon/index.js.map +1 -1
- package/dist/lexicon/lexicons.d.ts +10535 -7389
- package/dist/lexicon/lexicons.d.ts.map +1 -1
- package/dist/lexicon/lexicons.js +1789 -122
- package/dist/lexicon/lexicons.js.map +1 -1
- package/dist/lexicon/types/app/bsky/embed/external.d.ts +2 -0
- package/dist/lexicon/types/app/bsky/embed/external.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/embed/external.js.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/emitEvent.d.ts +19 -0
- package/dist/lexicon/types/tools/ozone/moderation/emitEvent.d.ts.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/emitEvent.js +9 -0
- package/dist/lexicon/types/tools/ozone/moderation/emitEvent.js.map +1 -1
- package/dist/lexicon/types/tools/ozone/queue/assignModerator.d.ts +27 -0
- package/dist/lexicon/types/tools/ozone/queue/assignModerator.d.ts.map +1 -0
- package/dist/lexicon/types/tools/ozone/queue/assignModerator.js +7 -0
- package/dist/lexicon/types/tools/ozone/queue/assignModerator.js.map +1 -0
- package/dist/lexicon/types/tools/ozone/queue/createQueue.d.ts +35 -0
- package/dist/lexicon/types/tools/ozone/queue/createQueue.d.ts.map +1 -0
- package/dist/lexicon/types/tools/ozone/queue/createQueue.js +7 -0
- package/dist/lexicon/types/tools/ozone/queue/createQueue.js.map +1 -0
- package/dist/lexicon/types/tools/ozone/queue/defs.d.ts +62 -0
- package/dist/lexicon/types/tools/ozone/queue/defs.d.ts.map +1 -0
- package/dist/lexicon/types/tools/ozone/queue/defs.js +34 -0
- package/dist/lexicon/types/tools/ozone/queue/defs.js.map +1 -0
- package/dist/lexicon/types/tools/ozone/queue/deleteQueue.d.ts +29 -0
- package/dist/lexicon/types/tools/ozone/queue/deleteQueue.d.ts.map +1 -0
- package/dist/lexicon/types/tools/ozone/queue/deleteQueue.js +7 -0
- package/dist/lexicon/types/tools/ozone/queue/deleteQueue.js.map +1 -0
- package/dist/lexicon/types/tools/ozone/queue/getAssignments.d.ts +30 -0
- package/dist/lexicon/types/tools/ozone/queue/getAssignments.d.ts.map +1 -0
- package/dist/lexicon/types/tools/ozone/queue/getAssignments.js +7 -0
- package/dist/lexicon/types/tools/ozone/queue/getAssignments.js.map +1 -0
- package/dist/lexicon/types/tools/ozone/queue/listQueues.d.ts +32 -0
- package/dist/lexicon/types/tools/ozone/queue/listQueues.d.ts.map +1 -0
- package/dist/lexicon/types/tools/ozone/queue/listQueues.js +7 -0
- package/dist/lexicon/types/tools/ozone/queue/listQueues.js.map +1 -0
- package/dist/lexicon/types/tools/ozone/queue/routeReports.d.ts +31 -0
- package/dist/lexicon/types/tools/ozone/queue/routeReports.d.ts.map +1 -0
- package/dist/lexicon/types/tools/ozone/queue/routeReports.js +7 -0
- package/dist/lexicon/types/tools/ozone/queue/routeReports.js.map +1 -0
- package/dist/lexicon/types/tools/ozone/queue/unassignModerator.d.ts +18 -0
- package/dist/lexicon/types/tools/ozone/queue/unassignModerator.d.ts.map +1 -0
- package/dist/lexicon/types/tools/ozone/queue/unassignModerator.js +7 -0
- package/dist/lexicon/types/tools/ozone/queue/unassignModerator.js.map +1 -0
- package/dist/lexicon/types/tools/ozone/queue/updateQueue.d.ts +32 -0
- package/dist/lexicon/types/tools/ozone/queue/updateQueue.d.ts.map +1 -0
- package/dist/lexicon/types/tools/ozone/queue/updateQueue.js +7 -0
- package/dist/lexicon/types/tools/ozone/queue/updateQueue.js.map +1 -0
- package/dist/lexicon/types/tools/ozone/report/assignModerator.d.ts +31 -0
- package/dist/lexicon/types/tools/ozone/report/assignModerator.d.ts.map +1 -0
- package/dist/lexicon/types/tools/ozone/report/assignModerator.js +7 -0
- package/dist/lexicon/types/tools/ozone/report/assignModerator.js.map +1 -0
- package/dist/lexicon/types/tools/ozone/report/createActivity.d.ts +37 -0
- package/dist/lexicon/types/tools/ozone/report/createActivity.d.ts.map +1 -0
- package/dist/lexicon/types/tools/ozone/report/createActivity.js +7 -0
- package/dist/lexicon/types/tools/ozone/report/createActivity.js.map +1 -0
- package/dist/lexicon/types/tools/ozone/report/defs.d.ts +185 -0
- package/dist/lexicon/types/tools/ozone/report/defs.d.ts.map +1 -1
- package/dist/lexicon/types/tools/ozone/report/defs.js +108 -0
- package/dist/lexicon/types/tools/ozone/report/defs.js.map +1 -1
- package/dist/lexicon/types/tools/ozone/report/getAssignments.d.ts +30 -0
- package/dist/lexicon/types/tools/ozone/report/getAssignments.d.ts.map +1 -0
- package/dist/lexicon/types/tools/ozone/report/getAssignments.js +7 -0
- package/dist/lexicon/types/tools/ozone/report/getAssignments.js.map +1 -0
- package/dist/lexicon/types/tools/ozone/report/getHistoricalStats.d.ts +36 -0
- package/dist/lexicon/types/tools/ozone/report/getHistoricalStats.d.ts.map +1 -0
- package/dist/lexicon/types/tools/ozone/report/getHistoricalStats.js +7 -0
- package/dist/lexicon/types/tools/ozone/report/getHistoricalStats.js.map +1 -0
- package/dist/lexicon/types/tools/ozone/report/getLatestReport.d.ts +21 -0
- package/dist/lexicon/types/tools/ozone/report/getLatestReport.d.ts.map +1 -0
- package/dist/lexicon/types/tools/ozone/report/getLatestReport.js +7 -0
- package/dist/lexicon/types/tools/ozone/report/getLatestReport.js.map +1 -0
- package/dist/lexicon/types/tools/ozone/report/getLiveStats.d.ts +27 -0
- package/dist/lexicon/types/tools/ozone/report/getLiveStats.d.ts.map +1 -0
- package/dist/lexicon/types/tools/ozone/report/getLiveStats.js +7 -0
- package/dist/lexicon/types/tools/ozone/report/getLiveStats.js.map +1 -0
- package/dist/lexicon/types/tools/ozone/report/getReport.d.ts +22 -0
- package/dist/lexicon/types/tools/ozone/report/getReport.d.ts.map +1 -0
- package/dist/lexicon/types/tools/ozone/report/getReport.js +7 -0
- package/dist/lexicon/types/tools/ozone/report/getReport.js.map +1 -0
- package/dist/lexicon/types/tools/ozone/report/listActivities.d.ts +26 -0
- package/dist/lexicon/types/tools/ozone/report/listActivities.d.ts.map +1 -0
- package/dist/lexicon/types/tools/ozone/report/listActivities.js +7 -0
- package/dist/lexicon/types/tools/ozone/report/listActivities.js.map +1 -0
- package/dist/lexicon/types/tools/ozone/report/queryReports.d.ts +48 -0
- package/dist/lexicon/types/tools/ozone/report/queryReports.d.ts.map +1 -0
- package/dist/lexicon/types/tools/ozone/report/queryReports.js +7 -0
- package/dist/lexicon/types/tools/ozone/report/queryReports.js.map +1 -0
- package/dist/lexicon/types/tools/ozone/report/reassignQueue.d.ts +31 -0
- package/dist/lexicon/types/tools/ozone/report/reassignQueue.d.ts.map +1 -0
- package/dist/lexicon/types/tools/ozone/report/reassignQueue.js +7 -0
- package/dist/lexicon/types/tools/ozone/report/reassignQueue.js.map +1 -0
- package/dist/lexicon/types/tools/ozone/report/refreshStats.d.ts +28 -0
- package/dist/lexicon/types/tools/ozone/report/refreshStats.d.ts.map +1 -0
- package/dist/lexicon/types/tools/ozone/report/refreshStats.js +7 -0
- package/dist/lexicon/types/tools/ozone/report/refreshStats.js.map +1 -0
- package/dist/lexicon/types/tools/ozone/report/unassignModerator.d.ts +25 -0
- package/dist/lexicon/types/tools/ozone/report/unassignModerator.d.ts.map +1 -0
- package/dist/lexicon/types/tools/ozone/report/unassignModerator.js +7 -0
- package/dist/lexicon/types/tools/ozone/report/unassignModerator.js.map +1 -0
- package/dist/mod-service/index.d.ts +3 -1
- package/dist/mod-service/index.d.ts.map +1 -1
- package/dist/mod-service/index.js +39 -2
- package/dist/mod-service/index.js.map +1 -1
- package/dist/mod-service/report.d.ts +64 -0
- package/dist/mod-service/report.d.ts.map +1 -0
- package/dist/mod-service/report.js +282 -0
- package/dist/mod-service/report.js.map +1 -0
- package/dist/mod-service/status.d.ts +20 -0
- package/dist/mod-service/status.d.ts.map +1 -1
- package/dist/queue/service.d.ts +86 -0
- package/dist/queue/service.d.ts.map +1 -0
- package/dist/queue/service.js +430 -0
- package/dist/queue/service.js.map +1 -0
- package/dist/report/activity.d.ts +77 -0
- package/dist/report/activity.d.ts.map +1 -0
- package/dist/report/activity.js +141 -0
- package/dist/report/activity.js.map +1 -0
- package/dist/report/handle-report-update.d.ts +47 -0
- package/dist/report/handle-report-update.d.ts.map +1 -0
- package/dist/report/handle-report-update.js +178 -0
- package/dist/report/handle-report-update.js.map +1 -0
- package/dist/report/reassign.d.ts +10 -0
- package/dist/report/reassign.d.ts.map +1 -0
- package/dist/report/reassign.js +75 -0
- package/dist/report/reassign.js.map +1 -0
- package/dist/report/stats.d.ts +105 -0
- package/dist/report/stats.d.ts.map +1 -0
- package/dist/report/stats.js +619 -0
- package/dist/report/stats.js.map +1 -0
- package/dist/report/views.d.ts +111 -0
- package/dist/report/views.d.ts.map +1 -0
- package/dist/report/views.js +156 -0
- package/dist/report/views.js.map +1 -0
- package/dist/team/index.d.ts +1 -0
- package/dist/team/index.d.ts.map +1 -1
- package/dist/team/index.js +11 -0
- package/dist/team/index.js.map +1 -1
- package/package.json +3 -3
- package/src/api/index.ts +40 -0
- package/src/api/moderation/emitEvent.ts +38 -0
- package/src/api/queue/assignModerator.ts +31 -0
- package/src/api/queue/createQueue.ts +62 -0
- package/src/api/queue/deleteQueue.ts +56 -0
- package/src/api/queue/getAssignments.ts +19 -0
- package/src/api/queue/listQueues.ts +39 -0
- package/src/api/queue/routeReports.ts +44 -0
- package/src/api/queue/unassignModerator.ts +26 -0
- package/src/api/queue/updateQueue.ts +54 -0
- package/src/api/report/assignModerator.ts +36 -0
- package/src/api/report/createActivity.ts +57 -0
- package/src/api/report/getAssignments.ts +20 -0
- package/src/api/report/getHistoricalStats.ts +41 -0
- package/src/api/report/getLatestReport.ts +44 -0
- package/src/api/report/getLiveStats.ts +26 -0
- package/src/api/report/getReport.ts +55 -0
- package/src/api/report/listActivities.ts +34 -0
- package/src/api/report/queryReports.ts +44 -0
- package/src/api/report/reassignQueue.ts +68 -0
- package/src/api/report/refreshStats.ts +27 -0
- package/src/api/report/unassignModerator.ts +21 -0
- package/src/api/util.ts +12 -0
- package/src/assignment/index.ts +731 -0
- package/src/config/config.ts +27 -0
- package/src/config/env.ts +8 -0
- package/src/context.ts +31 -0
- package/src/daemon/context.ts +34 -0
- package/src/daemon/job-cursor.ts +33 -0
- package/src/daemon/queue-router.ts +101 -0
- package/src/daemon/stats-computer.ts +101 -0
- package/src/daemon/strike-expiry-processor.ts +4 -20
- package/src/db/migrations/20260219T164523000Z-create-report-table.ts +155 -0
- package/src/db/migrations/20260219T165302248Z-moderator-assignment.ts +42 -0
- package/src/db/migrations/20260225T000000000Z-add-report-queue-table.ts +41 -0
- package/src/db/migrations/20260313T000000000Z-add-report-activity-table.ts +48 -0
- package/src/db/migrations/20260318T152058935Z-add-report-stat.ts +35 -0
- package/src/db/migrations/index.ts +5 -0
- package/src/db/pagination.ts +85 -0
- package/src/db/schema/index.ts +10 -0
- package/src/db/schema/moderator_assignment.ts +16 -0
- package/src/db/schema/report.ts +27 -0
- package/src/db/schema/report_activity.ts +22 -0
- package/src/db/schema/report_queue.ts +21 -0
- package/src/db/schema/report_stat.ts +27 -0
- package/src/lexicon/index.ts +280 -0
- package/src/lexicon/lexicons.ts +1910 -160
- package/src/lexicon/types/app/bsky/embed/external.ts +2 -0
- package/src/lexicon/types/tools/ozone/moderation/emitEvent.ts +24 -0
- package/src/lexicon/types/tools/ozone/queue/assignModerator.ts +46 -0
- package/src/lexicon/types/tools/ozone/queue/createQueue.ts +54 -0
- package/src/lexicon/types/tools/ozone/queue/defs.ts +99 -0
- package/src/lexicon/types/tools/ozone/queue/deleteQueue.ts +48 -0
- package/src/lexicon/types/tools/ozone/queue/getAssignments.ts +48 -0
- package/src/lexicon/types/tools/ozone/queue/listQueues.ts +50 -0
- package/src/lexicon/types/tools/ozone/queue/routeReports.ts +50 -0
- package/src/lexicon/types/tools/ozone/queue/unassignModerator.ts +37 -0
- package/src/lexicon/types/tools/ozone/queue/updateQueue.ts +51 -0
- package/src/lexicon/types/tools/ozone/report/assignModerator.ts +50 -0
- package/src/lexicon/types/tools/ozone/report/createActivity.ts +60 -0
- package/src/lexicon/types/tools/ozone/report/defs.ts +327 -0
- package/src/lexicon/types/tools/ozone/report/getAssignments.ts +48 -0
- package/src/lexicon/types/tools/ozone/report/getHistoricalStats.ts +54 -0
- package/src/lexicon/types/tools/ozone/report/getLatestReport.ts +39 -0
- package/src/lexicon/types/tools/ozone/report/getLiveStats.ts +45 -0
- package/src/lexicon/types/tools/ozone/report/getReport.ts +38 -0
- package/src/lexicon/types/tools/ozone/report/listActivities.ts +44 -0
- package/src/lexicon/types/tools/ozone/report/queryReports.ts +72 -0
- package/src/lexicon/types/tools/ozone/report/reassignQueue.ts +55 -0
- package/src/lexicon/types/tools/ozone/report/refreshStats.ts +46 -0
- package/src/lexicon/types/tools/ozone/report/unassignModerator.ts +44 -0
- package/src/mod-service/index.ts +45 -3
- package/src/mod-service/report.ts +408 -0
- package/src/queue/service.ts +599 -0
- package/src/report/activity.ts +234 -0
- package/src/report/handle-report-update.ts +209 -0
- package/src/report/reassign.ts +109 -0
- package/src/report/stats.ts +850 -0
- package/src/report/views.ts +241 -0
- package/src/team/index.ts +11 -0
- package/tests/get-report.test.ts +136 -0
- package/tests/query-reports.test.ts +608 -0
- package/tests/queue-assignment.test.ts +428 -0
- package/tests/queue-router.test.ts +306 -0
- package/tests/queues.test.ts +690 -0
- package/tests/report-action.test.ts +308 -0
- package/tests/report-activity.test.ts +567 -0
- package/tests/report-assignment.test.ts +517 -0
- package/tests/report-reassign-queue.test.ts +340 -0
- package/tests/report-routing.test.ts +245 -0
- package/tests/report-stats.test.ts +545 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.initJobCursor = initJobCursor;
|
|
4
|
+
exports.getJobCursor = getJobCursor;
|
|
5
|
+
exports.updateJobCursor = updateJobCursor;
|
|
6
|
+
async function initJobCursor(db, job) {
|
|
7
|
+
await db.db
|
|
8
|
+
.insertInto('job_cursor')
|
|
9
|
+
.values({ job, cursor: null })
|
|
10
|
+
.onConflict((oc) => oc.doNothing())
|
|
11
|
+
.execute();
|
|
12
|
+
}
|
|
13
|
+
async function getJobCursor(db, job) {
|
|
14
|
+
const entry = await db.db
|
|
15
|
+
.selectFrom('job_cursor')
|
|
16
|
+
.select('cursor')
|
|
17
|
+
.where('job', '=', job)
|
|
18
|
+
.executeTakeFirst();
|
|
19
|
+
return entry?.cursor ?? null;
|
|
20
|
+
}
|
|
21
|
+
async function updateJobCursor(db, job, cursor) {
|
|
22
|
+
await db.db
|
|
23
|
+
.updateTable('job_cursor')
|
|
24
|
+
.set({ cursor })
|
|
25
|
+
.where('job', '=', job)
|
|
26
|
+
.execute();
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=job-cursor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"job-cursor.js","sourceRoot":"","sources":["../../src/daemon/job-cursor.ts"],"names":[],"mappings":";;AAEA,sCAMC;AAED,oCAUC;AAED,0CAUC;AA9BM,KAAK,UAAU,aAAa,CAAC,EAAY,EAAE,GAAW;IAC3D,MAAM,EAAE,CAAC,EAAE;SACR,UAAU,CAAC,YAAY,CAAC;SACxB,MAAM,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;SAC7B,UAAU,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC;SAClC,OAAO,EAAE,CAAA;AACd,CAAC;AAEM,KAAK,UAAU,YAAY,CAChC,EAAY,EACZ,GAAW;IAEX,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,EAAE;SACtB,UAAU,CAAC,YAAY,CAAC;SACxB,MAAM,CAAC,QAAQ,CAAC;SAChB,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,CAAC;SACtB,gBAAgB,EAAE,CAAA;IACrB,OAAO,KAAK,EAAE,MAAM,IAAI,IAAI,CAAA;AAC9B,CAAC;AAEM,KAAK,UAAU,eAAe,CACnC,EAAY,EACZ,GAAW,EACX,MAAc;IAEd,MAAM,EAAE,CAAC,EAAE;SACR,WAAW,CAAC,YAAY,CAAC;SACzB,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;SACf,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,CAAC;SACtB,OAAO,EAAE,CAAA;AACd,CAAC","sourcesContent":["import { Database } from '../db'\n\nexport async function initJobCursor(db: Database, job: string): Promise<void> {\n await db.db\n .insertInto('job_cursor')\n .values({ job, cursor: null })\n .onConflict((oc) => oc.doNothing())\n .execute()\n}\n\nexport async function getJobCursor(\n db: Database,\n job: string,\n): Promise<string | null> {\n const entry = await db.db\n .selectFrom('job_cursor')\n .select('cursor')\n .where('job', '=', job)\n .executeTakeFirst()\n return entry?.cursor ?? null\n}\n\nexport async function updateJobCursor(\n db: Database,\n job: string,\n cursor: string,\n): Promise<void> {\n await db.db\n .updateTable('job_cursor')\n .set({ cursor })\n .where('job', '=', job)\n .execute()\n}\n"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Database } from '../db';
|
|
2
|
+
import { QueueServiceCreator } from '../queue/service';
|
|
3
|
+
export declare class QueueRouter {
|
|
4
|
+
private db;
|
|
5
|
+
private queueServiceCreator;
|
|
6
|
+
destroyed: boolean;
|
|
7
|
+
processingPromise: Promise<void>;
|
|
8
|
+
timer?: NodeJS.Timeout;
|
|
9
|
+
constructor(db: Database, queueServiceCreator: QueueServiceCreator);
|
|
10
|
+
start(): void;
|
|
11
|
+
poll(): void;
|
|
12
|
+
destroy(): Promise<void>;
|
|
13
|
+
initializeCursor(): Promise<void>;
|
|
14
|
+
getCursor(): Promise<number | null>;
|
|
15
|
+
routeReports(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=queue-router.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queue-router.d.ts","sourceRoot":"","sources":["../../src/daemon/queue-router.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEhC,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAMtD,qBAAa,WAAW;IAMpB,OAAO,CAAC,EAAE;IACV,OAAO,CAAC,mBAAmB;IAN7B,SAAS,UAAQ;IACjB,iBAAiB,EAAE,OAAO,CAAC,IAAI,CAAC,CAAoB;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC,OAAO,CAAA;gBAGZ,EAAE,EAAE,QAAQ,EACZ,mBAAmB,EAAE,mBAAmB;IAGlD,KAAK;IAIL,IAAI;IASE,OAAO;IASP,gBAAgB;IAIhB,SAAS,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IASnC,YAAY;CA2CnB"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.QueueRouter = void 0;
|
|
4
|
+
const common_1 = require("@atproto/common");
|
|
5
|
+
const logger_1 = require("../logger");
|
|
6
|
+
const job_cursor_1 = require("./job-cursor");
|
|
7
|
+
const JOB_NAME = 'queue_router';
|
|
8
|
+
const BATCH_SIZE = 100;
|
|
9
|
+
class QueueRouter {
|
|
10
|
+
constructor(db, queueServiceCreator) {
|
|
11
|
+
Object.defineProperty(this, "db", {
|
|
12
|
+
enumerable: true,
|
|
13
|
+
configurable: true,
|
|
14
|
+
writable: true,
|
|
15
|
+
value: db
|
|
16
|
+
});
|
|
17
|
+
Object.defineProperty(this, "queueServiceCreator", {
|
|
18
|
+
enumerable: true,
|
|
19
|
+
configurable: true,
|
|
20
|
+
writable: true,
|
|
21
|
+
value: queueServiceCreator
|
|
22
|
+
});
|
|
23
|
+
Object.defineProperty(this, "destroyed", {
|
|
24
|
+
enumerable: true,
|
|
25
|
+
configurable: true,
|
|
26
|
+
writable: true,
|
|
27
|
+
value: false
|
|
28
|
+
});
|
|
29
|
+
Object.defineProperty(this, "processingPromise", {
|
|
30
|
+
enumerable: true,
|
|
31
|
+
configurable: true,
|
|
32
|
+
writable: true,
|
|
33
|
+
value: Promise.resolve()
|
|
34
|
+
});
|
|
35
|
+
Object.defineProperty(this, "timer", {
|
|
36
|
+
enumerable: true,
|
|
37
|
+
configurable: true,
|
|
38
|
+
writable: true,
|
|
39
|
+
value: void 0
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
start() {
|
|
43
|
+
this.initializeCursor().then(() => this.poll());
|
|
44
|
+
}
|
|
45
|
+
poll() {
|
|
46
|
+
if (this.destroyed)
|
|
47
|
+
return;
|
|
48
|
+
this.processingPromise = this.routeReports()
|
|
49
|
+
.catch((err) => logger_1.dbLogger.error({ err }, 'queue routing errored'))
|
|
50
|
+
.finally(() => {
|
|
51
|
+
this.timer = setTimeout(() => this.poll(), getInterval());
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async destroy() {
|
|
55
|
+
this.destroyed = true;
|
|
56
|
+
if (this.timer) {
|
|
57
|
+
clearTimeout(this.timer);
|
|
58
|
+
this.timer = undefined;
|
|
59
|
+
}
|
|
60
|
+
await this.processingPromise;
|
|
61
|
+
}
|
|
62
|
+
async initializeCursor() {
|
|
63
|
+
await (0, job_cursor_1.initJobCursor)(this.db, JOB_NAME);
|
|
64
|
+
}
|
|
65
|
+
async getCursor() {
|
|
66
|
+
const row = await this.db.db
|
|
67
|
+
.selectFrom('job_cursor')
|
|
68
|
+
.select('cursor')
|
|
69
|
+
.where('job', '=', JOB_NAME)
|
|
70
|
+
.executeTakeFirst();
|
|
71
|
+
return row?.cursor ? parseInt(row.cursor, 10) : null;
|
|
72
|
+
}
|
|
73
|
+
async routeReports() {
|
|
74
|
+
await this.db.transaction(async (txn) => {
|
|
75
|
+
// Acquire row lock on the job_cursor row. A second daemon instance
|
|
76
|
+
// hitting this same query blocks here until the first transaction
|
|
77
|
+
// commits, then reads the now-advanced cursor and processes the next
|
|
78
|
+
// range. The lock is held for the whole batch (~50–200ms).
|
|
79
|
+
const row = await txn.db
|
|
80
|
+
.selectFrom('job_cursor')
|
|
81
|
+
.selectAll()
|
|
82
|
+
.where('job', '=', JOB_NAME)
|
|
83
|
+
.forUpdate()
|
|
84
|
+
.executeTakeFirst();
|
|
85
|
+
if (!row)
|
|
86
|
+
return;
|
|
87
|
+
const cursor = row.cursor ? parseInt(row.cursor, 10) : null;
|
|
88
|
+
const queueService = this.queueServiceCreator(txn);
|
|
89
|
+
const result = await queueService.insertReportsFromEvents({
|
|
90
|
+
cursor,
|
|
91
|
+
limit: BATCH_SIZE,
|
|
92
|
+
});
|
|
93
|
+
if (result.processed === 0) {
|
|
94
|
+
logger_1.dbLogger.info('no new report events to route');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
await txn.db
|
|
98
|
+
.updateTable('job_cursor')
|
|
99
|
+
.set({ cursor: String(result.maxEventId) })
|
|
100
|
+
.where('job', '=', JOB_NAME)
|
|
101
|
+
.execute();
|
|
102
|
+
logger_1.dbLogger.info({
|
|
103
|
+
processed: result.processed,
|
|
104
|
+
assigned: result.assigned,
|
|
105
|
+
unmatched: result.unmatched,
|
|
106
|
+
maxEventId: result.maxEventId,
|
|
107
|
+
}, 'queue routing completed');
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
exports.QueueRouter = QueueRouter;
|
|
112
|
+
// Poll every 1 minute
|
|
113
|
+
const getInterval = () => 1 * common_1.MINUTE;
|
|
114
|
+
//# sourceMappingURL=queue-router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queue-router.js","sourceRoot":"","sources":["../../src/daemon/queue-router.ts"],"names":[],"mappings":"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","sourcesContent":["import { MINUTE } from '@atproto/common'\nimport { Database } from '../db'\nimport { dbLogger } from '../logger'\nimport { QueueServiceCreator } from '../queue/service'\nimport { initJobCursor } from './job-cursor'\n\nconst JOB_NAME = 'queue_router'\nconst BATCH_SIZE = 100\n\nexport class QueueRouter {\n destroyed = false\n processingPromise: Promise<void> = Promise.resolve()\n timer?: NodeJS.Timeout\n\n constructor(\n private db: Database,\n private queueServiceCreator: QueueServiceCreator,\n ) {}\n\n start() {\n this.initializeCursor().then(() => this.poll())\n }\n\n poll() {\n if (this.destroyed) return\n this.processingPromise = this.routeReports()\n .catch((err) => dbLogger.error({ err }, 'queue routing errored'))\n .finally(() => {\n this.timer = setTimeout(() => this.poll(), getInterval())\n })\n }\n\n async destroy() {\n this.destroyed = true\n if (this.timer) {\n clearTimeout(this.timer)\n this.timer = undefined\n }\n await this.processingPromise\n }\n\n async initializeCursor() {\n await initJobCursor(this.db, JOB_NAME)\n }\n\n async getCursor(): Promise<number | null> {\n const row = await this.db.db\n .selectFrom('job_cursor')\n .select('cursor')\n .where('job', '=', JOB_NAME)\n .executeTakeFirst()\n return row?.cursor ? parseInt(row.cursor, 10) : null\n }\n\n async routeReports() {\n await this.db.transaction(async (txn) => {\n // Acquire row lock on the job_cursor row. A second daemon instance\n // hitting this same query blocks here until the first transaction\n // commits, then reads the now-advanced cursor and processes the next\n // range. The lock is held for the whole batch (~50–200ms).\n const row = await txn.db\n .selectFrom('job_cursor')\n .selectAll()\n .where('job', '=', JOB_NAME)\n .forUpdate()\n .executeTakeFirst()\n if (!row) return\n const cursor = row.cursor ? parseInt(row.cursor, 10) : null\n\n const queueService = this.queueServiceCreator(txn)\n const result = await queueService.insertReportsFromEvents({\n cursor,\n limit: BATCH_SIZE,\n })\n\n if (result.processed === 0) {\n dbLogger.info('no new report events to route')\n return\n }\n\n await txn.db\n .updateTable('job_cursor')\n .set({ cursor: String(result.maxEventId) })\n .where('job', '=', JOB_NAME)\n .execute()\n\n dbLogger.info(\n {\n processed: result.processed,\n assigned: result.assigned,\n unmatched: result.unmatched,\n maxEventId: result.maxEventId,\n },\n 'queue routing completed',\n )\n })\n }\n}\n\n// Poll every 1 minute\nconst getInterval = (): number => 1 * MINUTE\n"]}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Database } from '../db';
|
|
2
|
+
import { ReportStatsServiceCreator } from '../report/stats';
|
|
3
|
+
/**
|
|
4
|
+
* Background daemon that materializes report statistics on an interval (default is 15 minutes).
|
|
5
|
+
*
|
|
6
|
+
* Each cycle computes calendar-day snapshots: today's stats are recomputed (in-progress day),
|
|
7
|
+
* and yesterday's snapshot is finalized if it wasn't already. Historical snapshots (completed
|
|
8
|
+
* days) are write-once and never recomputed unless explicitly refreshed via the API.
|
|
9
|
+
*
|
|
10
|
+
* Query profile per cycle (assuming ~10K reports/day, 10 queues, 20 moderators, 9 type groups):
|
|
11
|
+
* - 7 batched GROUP BY queries against the report table for today's date window
|
|
12
|
+
* (+ 7 more for yesterday if finalization is needed).
|
|
13
|
+
* Day-window queries scan ~10K rows. Pending-count queries use partial indexes
|
|
14
|
+
* (WHERE status != 'closed') so only scan open reports, not the full table.
|
|
15
|
+
* Expected: ~10-50ms per query, ~100-350ms total report-table time.
|
|
16
|
+
* - ~40 lightweight reads against report_stat for freshness checks (small indexed table).
|
|
17
|
+
* - ~40 lightweight writes to report_stat for upserts.
|
|
18
|
+
*
|
|
19
|
+
* Locking: Uses pg_try_advisory_lock to ensure only one instance materializes at a time
|
|
20
|
+
* when running multiple containers. Advisory locks are cooperative, session-level locks —
|
|
21
|
+
* they do NOT block any table reads, writes, row locks, or transactions from other sessions.
|
|
22
|
+
* Normal application queries (report creation, moderation actions, API reads) are completely
|
|
23
|
+
* unaffected. If another instance already holds the lock, this instance skips the cycle
|
|
24
|
+
* immediately without blocking.
|
|
25
|
+
*/
|
|
26
|
+
export declare class StatsComputer {
|
|
27
|
+
private db;
|
|
28
|
+
private reportStatsServiceCreator;
|
|
29
|
+
/**
|
|
30
|
+
* Minutes between stats computer cycles.
|
|
31
|
+
* Defaults to 15. Minimum is 1.
|
|
32
|
+
* Set to -1 to disable the stats computer.
|
|
33
|
+
*/
|
|
34
|
+
private intervalMinutes;
|
|
35
|
+
destroyed: boolean;
|
|
36
|
+
processingPromise: Promise<void>;
|
|
37
|
+
timer?: NodeJS.Timeout;
|
|
38
|
+
constructor(db: Database, reportStatsServiceCreator: ReportStatsServiceCreator,
|
|
39
|
+
/**
|
|
40
|
+
* Minutes between stats computer cycles.
|
|
41
|
+
* Defaults to 15. Minimum is 1.
|
|
42
|
+
* Set to -1 to disable the stats computer.
|
|
43
|
+
*/
|
|
44
|
+
intervalMinutes: number);
|
|
45
|
+
get disabled(): boolean;
|
|
46
|
+
start(): void;
|
|
47
|
+
poll(): void;
|
|
48
|
+
private materializeStats;
|
|
49
|
+
destroy(): Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=stats-computer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stats-computer.d.ts","sourceRoot":"","sources":["../../src/daemon/stats-computer.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEhC,OAAO,EAAE,yBAAyB,EAAE,MAAM,iBAAiB,CAAA;AAK3D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,aAAa;IAMtB,OAAO,CAAC,EAAE;IACV,OAAO,CAAC,yBAAyB;IACjC;;;;OAIG;IACH,OAAO,CAAC,eAAe;IAZzB,SAAS,UAAQ;IACjB,iBAAiB,EAAE,OAAO,CAAC,IAAI,CAAC,CAAoB;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC,OAAO,CAAA;gBAGZ,EAAE,EAAE,QAAQ,EACZ,yBAAyB,EAAE,yBAAyB;IAC5D;;;;OAIG;IACK,eAAe,EAAE,MAAM;IAGjC,IAAI,QAAQ,YAEX;IAED,KAAK;IAIL,IAAI;YAYU,gBAAgB;IAwBxB,OAAO;CAQd"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.StatsComputer = void 0;
|
|
4
|
+
const kysely_1 = require("kysely");
|
|
5
|
+
const common_1 = require("@atproto/common");
|
|
6
|
+
const logger_1 = require("../logger");
|
|
7
|
+
// Stable lock ID for pg_try_advisory_lock across all instances
|
|
8
|
+
const ADVISORY_LOCK_ID = 7239401;
|
|
9
|
+
/**
|
|
10
|
+
* Background daemon that materializes report statistics on an interval (default is 15 minutes).
|
|
11
|
+
*
|
|
12
|
+
* Each cycle computes calendar-day snapshots: today's stats are recomputed (in-progress day),
|
|
13
|
+
* and yesterday's snapshot is finalized if it wasn't already. Historical snapshots (completed
|
|
14
|
+
* days) are write-once and never recomputed unless explicitly refreshed via the API.
|
|
15
|
+
*
|
|
16
|
+
* Query profile per cycle (assuming ~10K reports/day, 10 queues, 20 moderators, 9 type groups):
|
|
17
|
+
* - 7 batched GROUP BY queries against the report table for today's date window
|
|
18
|
+
* (+ 7 more for yesterday if finalization is needed).
|
|
19
|
+
* Day-window queries scan ~10K rows. Pending-count queries use partial indexes
|
|
20
|
+
* (WHERE status != 'closed') so only scan open reports, not the full table.
|
|
21
|
+
* Expected: ~10-50ms per query, ~100-350ms total report-table time.
|
|
22
|
+
* - ~40 lightweight reads against report_stat for freshness checks (small indexed table).
|
|
23
|
+
* - ~40 lightweight writes to report_stat for upserts.
|
|
24
|
+
*
|
|
25
|
+
* Locking: Uses pg_try_advisory_lock to ensure only one instance materializes at a time
|
|
26
|
+
* when running multiple containers. Advisory locks are cooperative, session-level locks —
|
|
27
|
+
* they do NOT block any table reads, writes, row locks, or transactions from other sessions.
|
|
28
|
+
* Normal application queries (report creation, moderation actions, API reads) are completely
|
|
29
|
+
* unaffected. If another instance already holds the lock, this instance skips the cycle
|
|
30
|
+
* immediately without blocking.
|
|
31
|
+
*/
|
|
32
|
+
class StatsComputer {
|
|
33
|
+
constructor(db, reportStatsServiceCreator,
|
|
34
|
+
/**
|
|
35
|
+
* Minutes between stats computer cycles.
|
|
36
|
+
* Defaults to 15. Minimum is 1.
|
|
37
|
+
* Set to -1 to disable the stats computer.
|
|
38
|
+
*/
|
|
39
|
+
intervalMinutes) {
|
|
40
|
+
Object.defineProperty(this, "db", {
|
|
41
|
+
enumerable: true,
|
|
42
|
+
configurable: true,
|
|
43
|
+
writable: true,
|
|
44
|
+
value: db
|
|
45
|
+
});
|
|
46
|
+
Object.defineProperty(this, "reportStatsServiceCreator", {
|
|
47
|
+
enumerable: true,
|
|
48
|
+
configurable: true,
|
|
49
|
+
writable: true,
|
|
50
|
+
value: reportStatsServiceCreator
|
|
51
|
+
});
|
|
52
|
+
Object.defineProperty(this, "intervalMinutes", {
|
|
53
|
+
enumerable: true,
|
|
54
|
+
configurable: true,
|
|
55
|
+
writable: true,
|
|
56
|
+
value: intervalMinutes
|
|
57
|
+
});
|
|
58
|
+
Object.defineProperty(this, "destroyed", {
|
|
59
|
+
enumerable: true,
|
|
60
|
+
configurable: true,
|
|
61
|
+
writable: true,
|
|
62
|
+
value: false
|
|
63
|
+
});
|
|
64
|
+
Object.defineProperty(this, "processingPromise", {
|
|
65
|
+
enumerable: true,
|
|
66
|
+
configurable: true,
|
|
67
|
+
writable: true,
|
|
68
|
+
value: Promise.resolve()
|
|
69
|
+
});
|
|
70
|
+
Object.defineProperty(this, "timer", {
|
|
71
|
+
enumerable: true,
|
|
72
|
+
configurable: true,
|
|
73
|
+
writable: true,
|
|
74
|
+
value: void 0
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
get disabled() {
|
|
78
|
+
return this.intervalMinutes < 1;
|
|
79
|
+
}
|
|
80
|
+
start() {
|
|
81
|
+
this.poll();
|
|
82
|
+
}
|
|
83
|
+
poll() {
|
|
84
|
+
if (this.destroyed || this.disabled)
|
|
85
|
+
return;
|
|
86
|
+
this.processingPromise = this.materializeStats()
|
|
87
|
+
.catch((err) => logger_1.dbLogger.error({ err }, 'stats materialization errored'))
|
|
88
|
+
.finally(() => {
|
|
89
|
+
this.timer = setTimeout(() => this.poll(), this.intervalMinutes * common_1.MINUTE);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async materializeStats() {
|
|
93
|
+
const lockResult = await (0, kysely_1.sql) `SELECT pg_try_advisory_lock(${ADVISORY_LOCK_ID}) as locked`.execute(this.db.db);
|
|
94
|
+
const acquired = lockResult.rows[0]?.locked === true;
|
|
95
|
+
if (!acquired) {
|
|
96
|
+
logger_1.dbLogger.info('stats materialization skipped, another instance holds lock');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const statsService = this.reportStatsServiceCreator(this.db);
|
|
101
|
+
await statsService.materializeAll();
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
await (0, kysely_1.sql) `SELECT pg_advisory_unlock(${ADVISORY_LOCK_ID})`.execute(this.db.db);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async destroy() {
|
|
108
|
+
this.destroyed = true;
|
|
109
|
+
if (this.timer) {
|
|
110
|
+
clearTimeout(this.timer);
|
|
111
|
+
this.timer = undefined;
|
|
112
|
+
}
|
|
113
|
+
await this.processingPromise;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
exports.StatsComputer = StatsComputer;
|
|
117
|
+
//# sourceMappingURL=stats-computer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stats-computer.js","sourceRoot":"","sources":["../../src/daemon/stats-computer.ts"],"names":[],"mappings":";;;AAAA,mCAA4B;AAC5B,4CAAwC;AAExC,sCAAoC;AAGpC,+DAA+D;AAC/D,MAAM,gBAAgB,GAAG,OAAS,CAAA;AAElC;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAa,aAAa;IAKxB,YACU,EAAY,EACZ,yBAAoD;IAC5D;;;;OAIG;IACK,eAAuB;QAP/B;;;;mBAAQ,EAAE;WAAU;QACpB;;;;mBAAQ,yBAAyB;WAA2B;QAM5D;;;;mBAAQ,eAAe;WAAQ;QAZjC;;;;mBAAY,KAAK;WAAA;QACjB;;;;mBAAmC,OAAO,CAAC,OAAO,EAAE;WAAA;QACpD;;;;;WAAsB;IAWnB,CAAC;IAEJ,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,eAAe,GAAG,CAAC,CAAA;IACjC,CAAC;IAED,KAAK;QACH,IAAI,CAAC,IAAI,EAAE,CAAA;IACb,CAAC;IAED,IAAI;QACF,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAM;QAC3C,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAC,gBAAgB,EAAE;aAC7C,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,iBAAQ,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,+BAA+B,CAAC,CAAC;aACxE,OAAO,CAAC,GAAG,EAAE;YACZ,IAAI,CAAC,KAAK,GAAG,UAAU,CACrB,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,EACjB,IAAI,CAAC,eAAe,GAAG,eAAM,CAC9B,CAAA;QACH,CAAC,CAAC,CAAA;IACN,CAAC;IAEO,KAAK,CAAC,gBAAgB;QAC5B,MAAM,UAAU,GAAG,MAAM,IAAA,YAAG,EAE1B,+BAA+B,gBAAgB,aAAa,CAAC,OAAO,CACpE,IAAI,CAAC,EAAE,CAAC,EAAE,CACX,CAAA;QACD,MAAM,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;QACpD,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,iBAAQ,CAAC,IAAI,CACX,4DAA4D,CAC7D,CAAA;YACD,OAAM;QACR,CAAC;QAED,IAAI,CAAC;YACH,MAAM,YAAY,GAAG,IAAI,CAAC,yBAAyB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;YAC5D,MAAM,YAAY,CAAC,cAAc,EAAE,CAAA;QACrC,CAAC;gBAAS,CAAC;YACT,MAAM,IAAA,YAAG,EAAA,6BAA6B,gBAAgB,GAAG,CAAC,OAAO,CAC/D,IAAI,CAAC,EAAE,CAAC,EAAE,CACX,CAAA;QACH,CAAC;IACH,CAAC;IAED,KAAK,CAAC,OAAO;QACX,IAAI,CAAC,SAAS,GAAG,IAAI,CAAA;QACrB,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;YACxB,IAAI,CAAC,KAAK,GAAG,SAAS,CAAA;QACxB,CAAC;QACD,MAAM,IAAI,CAAC,iBAAiB,CAAA;IAC9B,CAAC;CACF;AApED,sCAoEC","sourcesContent":["import { sql } from 'kysely'\nimport { MINUTE } from '@atproto/common'\nimport { Database } from '../db'\nimport { dbLogger } from '../logger'\nimport { ReportStatsServiceCreator } from '../report/stats'\n\n// Stable lock ID for pg_try_advisory_lock across all instances\nconst ADVISORY_LOCK_ID = 7_239_401\n\n/**\n * Background daemon that materializes report statistics on an interval (default is 15 minutes).\n *\n * Each cycle computes calendar-day snapshots: today's stats are recomputed (in-progress day),\n * and yesterday's snapshot is finalized if it wasn't already. Historical snapshots (completed\n * days) are write-once and never recomputed unless explicitly refreshed via the API.\n *\n * Query profile per cycle (assuming ~10K reports/day, 10 queues, 20 moderators, 9 type groups):\n * - 7 batched GROUP BY queries against the report table for today's date window\n * (+ 7 more for yesterday if finalization is needed).\n * Day-window queries scan ~10K rows. Pending-count queries use partial indexes\n * (WHERE status != 'closed') so only scan open reports, not the full table.\n * Expected: ~10-50ms per query, ~100-350ms total report-table time.\n * - ~40 lightweight reads against report_stat for freshness checks (small indexed table).\n * - ~40 lightweight writes to report_stat for upserts.\n *\n * Locking: Uses pg_try_advisory_lock to ensure only one instance materializes at a time\n * when running multiple containers. Advisory locks are cooperative, session-level locks —\n * they do NOT block any table reads, writes, row locks, or transactions from other sessions.\n * Normal application queries (report creation, moderation actions, API reads) are completely\n * unaffected. If another instance already holds the lock, this instance skips the cycle\n * immediately without blocking.\n */\nexport class StatsComputer {\n destroyed = false\n processingPromise: Promise<void> = Promise.resolve()\n timer?: NodeJS.Timeout\n\n constructor(\n private db: Database,\n private reportStatsServiceCreator: ReportStatsServiceCreator,\n /**\n * Minutes between stats computer cycles.\n * Defaults to 15. Minimum is 1.\n * Set to -1 to disable the stats computer.\n */\n private intervalMinutes: number,\n ) {}\n\n get disabled() {\n return this.intervalMinutes < 1\n }\n\n start() {\n this.poll()\n }\n\n poll() {\n if (this.destroyed || this.disabled) return\n this.processingPromise = this.materializeStats()\n .catch((err) => dbLogger.error({ err }, 'stats materialization errored'))\n .finally(() => {\n this.timer = setTimeout(\n () => this.poll(),\n this.intervalMinutes * MINUTE,\n )\n })\n }\n\n private async materializeStats() {\n const lockResult = await sql<{\n locked: boolean\n }>`SELECT pg_try_advisory_lock(${ADVISORY_LOCK_ID}) as locked`.execute(\n this.db.db,\n )\n const acquired = lockResult.rows[0]?.locked === true\n if (!acquired) {\n dbLogger.info(\n 'stats materialization skipped, another instance holds lock',\n )\n return\n }\n\n try {\n const statsService = this.reportStatsServiceCreator(this.db)\n await statsService.materializeAll()\n } finally {\n await sql`SELECT pg_advisory_unlock(${ADVISORY_LOCK_ID})`.execute(\n this.db.db,\n )\n }\n }\n\n async destroy() {\n this.destroyed = true\n if (this.timer) {\n clearTimeout(this.timer)\n this.timer = undefined\n }\n await this.processingPromise\n }\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"strike-expiry-processor.d.ts","sourceRoot":"","sources":["../../src/daemon/strike-expiry-processor.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEhC,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"strike-expiry-processor.d.ts","sourceRoot":"","sources":["../../src/daemon/strike-expiry-processor.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEhC,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AAK5D,qBAAa,qBAAqB;IAM9B,OAAO,CAAC,EAAE;IACV,OAAO,CAAC,oBAAoB;IAN9B,SAAS,UAAQ;IACjB,iBAAiB,EAAE,OAAO,CAAC,IAAI,CAAC,CAAoB;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC,OAAO,CAAA;gBAGZ,EAAE,EAAE,QAAQ,EACZ,oBAAoB,EAAE,oBAAoB;IAGpD,KAAK;IAIL,IAAI;IAWE,OAAO;IASP,gBAAgB;IAIhB,SAAS,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAInC,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI3C,qBAAqB;CAgC5B"}
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.StrikeExpiryProcessor = void 0;
|
|
4
4
|
const common_1 = require("@atproto/common");
|
|
5
5
|
const logger_1 = require("../logger");
|
|
6
|
+
const job_cursor_1 = require("./job-cursor");
|
|
6
7
|
const JOB_NAME = 'strike_expiry';
|
|
7
8
|
class StrikeExpiryProcessor {
|
|
8
9
|
constructor(db, strikeServiceCreator) {
|
|
@@ -58,29 +59,13 @@ class StrikeExpiryProcessor {
|
|
|
58
59
|
await this.processingPromise;
|
|
59
60
|
}
|
|
60
61
|
async initializeCursor() {
|
|
61
|
-
await this.db
|
|
62
|
-
.insertInto('job_cursor')
|
|
63
|
-
.values({
|
|
64
|
-
job: JOB_NAME,
|
|
65
|
-
cursor: null,
|
|
66
|
-
})
|
|
67
|
-
.onConflict((oc) => oc.doNothing())
|
|
68
|
-
.execute();
|
|
62
|
+
await (0, job_cursor_1.initJobCursor)(this.db, JOB_NAME);
|
|
69
63
|
}
|
|
70
64
|
async getCursor() {
|
|
71
|
-
|
|
72
|
-
.selectFrom('job_cursor')
|
|
73
|
-
.select('cursor')
|
|
74
|
-
.where('job', '=', JOB_NAME)
|
|
75
|
-
.executeTakeFirst();
|
|
76
|
-
return entry?.cursor || null;
|
|
65
|
+
return (0, job_cursor_1.getJobCursor)(this.db, JOB_NAME);
|
|
77
66
|
}
|
|
78
67
|
async updateCursor(cursor) {
|
|
79
|
-
await this.db
|
|
80
|
-
.updateTable('job_cursor')
|
|
81
|
-
.set({ cursor })
|
|
82
|
-
.where('job', '=', JOB_NAME)
|
|
83
|
-
.execute();
|
|
68
|
+
await (0, job_cursor_1.updateJobCursor)(this.db, JOB_NAME, cursor);
|
|
84
69
|
}
|
|
85
70
|
async processExpiredStrikes() {
|
|
86
71
|
const now = new Date();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"strike-expiry-processor.js","sourceRoot":"","sources":["../../src/daemon/strike-expiry-processor.ts"],"names":[],"mappings":";;;AAAA,4CAAsC;AAEtC,sCAAoC;
|
|
1
|
+
{"version":3,"file":"strike-expiry-processor.js","sourceRoot":"","sources":["../../src/daemon/strike-expiry-processor.ts"],"names":[],"mappings":";;;AAAA,4CAAsC;AAEtC,sCAAoC;AAEpC,6CAA2E;AAE3E,MAAM,QAAQ,GAAG,eAAe,CAAA;AAEhC,MAAa,qBAAqB;IAKhC,YACU,EAAY,EACZ,oBAA0C;QADlD;;;;mBAAQ,EAAE;WAAU;QACpB;;;;mBAAQ,oBAAoB;WAAsB;QANpD;;;;mBAAY,KAAK;WAAA;QACjB;;;;mBAAmC,OAAO,CAAC,OAAO,EAAE;WAAA;QACpD;;;;;WAAsB;IAKnB,CAAC;IAEJ,KAAK;QACH,IAAI,CAAC,gBAAgB,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAA;IACjD,CAAC;IAED,IAAI;QACF,IAAI,IAAI,CAAC,SAAS;YAAE,OAAM;QAC1B,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAC,qBAAqB,EAAE;aAClD,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CACb,iBAAQ,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,kCAAkC,CAAC,CAC5D;aACA,OAAO,CAAC,GAAG,EAAE;YACZ,IAAI,CAAC,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,WAAW,EAAE,CAAC,CAAA;QAC3D,CAAC,CAAC,CAAA;IACN,CAAC;IAED,KAAK,CAAC,OAAO;QACX,IAAI,CAAC,SAAS,GAAG,IAAI,CAAA;QACrB,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;YACxB,IAAI,CAAC,KAAK,GAAG,SAAS,CAAA;QACxB,CAAC;QACD,MAAM,IAAI,CAAC,iBAAiB,CAAA;IAC9B,CAAC;IAED,KAAK,CAAC,gBAAgB;QACpB,MAAM,IAAA,0BAAa,EAAC,IAAI,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAA;IACxC,CAAC;IAED,KAAK,CAAC,SAAS;QACb,OAAO,IAAA,yBAAY,EAAC,IAAI,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAA;IACxC,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,MAAc;QAC/B,MAAM,IAAA,4BAAe,EAAC,IAAI,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAA;IAClD,CAAC;IAED,KAAK,CAAC,qBAAqB;QACzB,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAA;QACtB,MAAM,aAAa,GAAG,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QACxD,MAAM,eAAe,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAA;QAC9C,MAAM,gBAAgB,GAAG,MAAM,aAAa,CAAC,wBAAwB,CACnE,eAAe,IAAI,SAAS,CAC7B,CAAA;QAED,IAAI,CAAC,gBAAgB,CAAC,MAAM,EAAE,CAAC;YAC7B,iBAAQ,CAAC,IAAI,CAAC,+BAA+B,CAAC,CAAA;YAC9C,MAAM,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,CAAA;YAC1C,OAAM;QACR,CAAC;QAED,iBAAQ,CAAC,IAAI,CACX,EAAE,KAAK,EAAE,gBAAgB,CAAC,MAAM,EAAE,EAClC,0CAA0C,CAC3C,CAAA;QAED,MAAM,OAAO,CAAC,GAAG,CACf,gBAAgB,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE;YACtC,OAAO,aAAa,CAAC,wBAAwB,CAAC,UAAU,CAAC,CAAA;QAC3D,CAAC,CAAC,CACH,CAAA;QAED,MAAM,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,CAAA;QAE1C,iBAAQ,CAAC,IAAI,CACX,EAAE,SAAS,EAAE,gBAAgB,CAAC,MAAM,EAAE,EACtC,oCAAoC,CACrC,CAAA;IACH,CAAC;CACF;AA9ED,sDA8EC;AAED,MAAM,WAAW,GAAG,GAAW,EAAE;IAC/B,oDAAoD;IACpD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;IACtB,MAAM,UAAU,GAAG,aAAI,CAAA;IACvB,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,GAAG,UAAU,CAAC,CAAA;IACjD,OAAO,aAAa,GAAG,UAAU,GAAG,GAAG,CAAA;AACzC,CAAC,CAAA","sourcesContent":["import { HOUR } from '@atproto/common'\nimport { Database } from '../db'\nimport { dbLogger } from '../logger'\nimport { StrikeServiceCreator } from '../mod-service/strike'\nimport { getJobCursor, initJobCursor, updateJobCursor } from './job-cursor'\n\nconst JOB_NAME = 'strike_expiry'\n\nexport class StrikeExpiryProcessor {\n destroyed = false\n processingPromise: Promise<void> = Promise.resolve()\n timer?: NodeJS.Timeout\n\n constructor(\n private db: Database,\n private strikeServiceCreator: StrikeServiceCreator,\n ) {}\n\n start() {\n this.initializeCursor().then(() => this.poll())\n }\n\n poll() {\n if (this.destroyed) return\n this.processingPromise = this.processExpiredStrikes()\n .catch((err) =>\n dbLogger.error({ err }, 'strike expiry processing errored'),\n )\n .finally(() => {\n this.timer = setTimeout(() => this.poll(), getInterval())\n })\n }\n\n async destroy() {\n this.destroyed = true\n if (this.timer) {\n clearTimeout(this.timer)\n this.timer = undefined\n }\n await this.processingPromise\n }\n\n async initializeCursor() {\n await initJobCursor(this.db, JOB_NAME)\n }\n\n async getCursor(): Promise<string | null> {\n return getJobCursor(this.db, JOB_NAME)\n }\n\n async updateCursor(cursor: string): Promise<void> {\n await updateJobCursor(this.db, JOB_NAME, cursor)\n }\n\n async processExpiredStrikes() {\n const now = new Date()\n const strikeService = this.strikeServiceCreator(this.db)\n const lastProcessedAt = await this.getCursor()\n const affectedSubjects = await strikeService.getExpiredStrikeSubjects(\n lastProcessedAt || undefined,\n )\n\n if (!affectedSubjects.length) {\n dbLogger.info('no expired strikes to process')\n await this.updateCursor(now.toISOString())\n return\n }\n\n dbLogger.info(\n { count: affectedSubjects.length },\n 'processing subjects with expired strikes',\n )\n\n await Promise.all(\n affectedSubjects.map(({ subjectDid }) => {\n return strikeService.updateSubjectStrikeCount(subjectDid)\n }),\n )\n\n await this.updateCursor(now.toISOString())\n\n dbLogger.info(\n { processed: affectedSubjects.length },\n 'strike expiry processing completed',\n )\n }\n}\n\nconst getInterval = (): number => {\n // Run every hour, synchronized to the hour boundary\n const now = Date.now()\n const intervalMs = HOUR\n const nextIteration = Math.ceil(now / intervalMs)\n return nextIteration * intervalMs - now\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260219T164523000Z-create-report-table.d.ts","sourceRoot":"","sources":["../../../src/db/migrations/20260219T164523000Z-create-report-table.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAO,MAAM,QAAQ,CAAA;AAEpC,wBAAsB,EAAE,CAAC,EAAE,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAmJ3D;AAED,wBAAsB,IAAI,CAAC,EAAE,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAG7D"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.up = up;
|
|
4
|
+
exports.down = down;
|
|
5
|
+
const kysely_1 = require("kysely");
|
|
6
|
+
async function up(db) {
|
|
7
|
+
// Report table - bridges report events to action events
|
|
8
|
+
await db.schema
|
|
9
|
+
.createTable('report')
|
|
10
|
+
.addColumn('id', 'serial', (col) => col.primaryKey())
|
|
11
|
+
// Core link to report event (display data still comes from moderation_event via JOIN)
|
|
12
|
+
.addColumn('eventId', 'integer', (col) => col.notNull().unique())
|
|
13
|
+
// Queue assignment (computed by background job in future iteration)
|
|
14
|
+
.addColumn('queueId', 'integer') // NULL = not yet assigned, -1 = no matching queue
|
|
15
|
+
.addColumn('queuedAt', 'varchar')
|
|
16
|
+
// Action linkage (sorted DESC, most recent first)
|
|
17
|
+
.addColumn('actionEventIds', 'jsonb') // Array of event IDs: [newest_id, ..., oldest_id]
|
|
18
|
+
// Reporter communication
|
|
19
|
+
.addColumn('actionNote', 'text')
|
|
20
|
+
// Whether the report is muted (reporter was muted or subject was muted at creation time)
|
|
21
|
+
.addColumn('isMuted', 'boolean', (col) => col.notNull().defaultTo(false))
|
|
22
|
+
// Status of the ticket/report
|
|
23
|
+
.addColumn('status', 'varchar', (col) => col.notNull().defaultTo('open')) // "open", "closed", "escalated"
|
|
24
|
+
// Denormalized from moderation_event for filtering without JOIN
|
|
25
|
+
.addColumn('reportType', 'varchar', (col) => col.notNull())
|
|
26
|
+
.addColumn('did', 'varchar', (col) => col.notNull())
|
|
27
|
+
.addColumn('recordPath', 'varchar', (col) => col.notNull().defaultTo('')) // '' = account/message, 'collection/rkey' = record
|
|
28
|
+
.addColumn('subjectMessageId', 'varchar') // NULL for non-message subjects
|
|
29
|
+
// Timestamps
|
|
30
|
+
.addColumn('createdAt', 'varchar', (col) => col.notNull())
|
|
31
|
+
.addColumn('updatedAt', 'varchar', (col) => col.notNull())
|
|
32
|
+
.addColumn('assignedTo', 'varchar') // DID of permanently assigned moderator
|
|
33
|
+
.addColumn('assignedAt', 'varchar') // When the permanent assignment was created
|
|
34
|
+
.addColumn('closedAt', 'varchar')
|
|
35
|
+
.execute();
|
|
36
|
+
// ─── Indexes ───
|
|
37
|
+
// Primary JOIN index - critical for every query that fetches display data from moderation_event
|
|
38
|
+
await db.schema
|
|
39
|
+
.createIndex('idx_report_event')
|
|
40
|
+
.on('report')
|
|
41
|
+
.column('eventId')
|
|
42
|
+
.execute();
|
|
43
|
+
// ─── Hot path: active reports (status != 'closed') ───
|
|
44
|
+
// Partial filter keeps these tight even as closed reports accumulate (~90% of table long-term).
|
|
45
|
+
// No isMuted in key (low cardinality, rarely filtered) and no INCLUDE columns
|
|
46
|
+
// (display data comes from moderation_event JOIN anyway).
|
|
47
|
+
// queryReports: queueId + status, paginated by createdAt
|
|
48
|
+
await (0, kysely_1.sql) `CREATE INDEX idx_report_active_queue_created ON report
|
|
49
|
+
("queueId", status, "createdAt" DESC, id DESC)
|
|
50
|
+
WHERE status != 'closed'`.execute(db);
|
|
51
|
+
// queryReports: queueId + status, paginated by updatedAt
|
|
52
|
+
await (0, kysely_1.sql) `CREATE INDEX idx_report_active_queue_updated ON report
|
|
53
|
+
("queueId", status, "updatedAt" DESC, id DESC)
|
|
54
|
+
WHERE status != 'closed'`.execute(db);
|
|
55
|
+
// queryReports: status only, paginated by createdAt
|
|
56
|
+
await (0, kysely_1.sql) `CREATE INDEX idx_report_active_status_created ON report
|
|
57
|
+
(status, "createdAt" DESC, id DESC)
|
|
58
|
+
WHERE status != 'closed'`.execute(db);
|
|
59
|
+
// queryReports: status only, paginated by updatedAt
|
|
60
|
+
await (0, kysely_1.sql) `CREATE INDEX idx_report_active_status_updated ON report
|
|
61
|
+
(status, "updatedAt" DESC, id DESC)
|
|
62
|
+
WHERE status != 'closed'`.execute(db);
|
|
63
|
+
// Active reports for a specific account (with optional queueId post-filter)
|
|
64
|
+
await (0, kysely_1.sql) `CREATE INDEX idx_report_active_did_created ON report
|
|
65
|
+
(did, status, "createdAt" DESC, id DESC)
|
|
66
|
+
WHERE status != 'closed'`.execute(db);
|
|
67
|
+
// A moderator's active workload (with optional queueId post-filter)
|
|
68
|
+
await (0, kysely_1.sql) `CREATE INDEX idx_report_active_assigned_created ON report
|
|
69
|
+
("assignedTo", status, "createdAt" DESC, id DESC)
|
|
70
|
+
WHERE status != 'closed'`.execute(db);
|
|
71
|
+
// findReportsForSubject hot path — always filters NOT IN ('closed').
|
|
72
|
+
// did + recordPath identify the subject (account or specific record).
|
|
73
|
+
await (0, kysely_1.sql) `CREATE INDEX idx_report_subject_active ON report
|
|
74
|
+
(did, "recordPath", "createdAt" DESC, id DESC)
|
|
75
|
+
WHERE status != 'closed'`.execute(db);
|
|
76
|
+
// ─── Closed history (status = 'closed') ───
|
|
77
|
+
// Closed reports are terminal; only sort by createdAt.
|
|
78
|
+
// Closed pagination per queue
|
|
79
|
+
await (0, kysely_1.sql) `CREATE INDEX idx_report_closed_queue_created ON report
|
|
80
|
+
("queueId", "createdAt" DESC, id DESC)
|
|
81
|
+
WHERE status = 'closed'`.execute(db);
|
|
82
|
+
// Closed history for an account
|
|
83
|
+
await (0, kysely_1.sql) `CREATE INDEX idx_report_closed_did_created ON report
|
|
84
|
+
(did, "createdAt" DESC, id DESC)
|
|
85
|
+
WHERE status = 'closed'`.execute(db);
|
|
86
|
+
// Moderator's closed-report history
|
|
87
|
+
await (0, kysely_1.sql) `CREATE INDEX idx_report_closed_assigned_created ON report
|
|
88
|
+
("assignedTo", "createdAt" DESC, id DESC)
|
|
89
|
+
WHERE status = 'closed'`.execute(db);
|
|
90
|
+
// ─── Other access patterns ───
|
|
91
|
+
// Collection prefix queries: left-anchored LIKE 'app.bsky.feed.post/%' or 'app.bsky.%'
|
|
92
|
+
// text_pattern_ops enables btree-scannable prefix matching (supported since Postgres 8.x)
|
|
93
|
+
await (0, kysely_1.sql) `CREATE INDEX idx_report_record_path_pattern ON report
|
|
94
|
+
("recordPath" text_pattern_ops)`.execute(db);
|
|
95
|
+
// Queue-router covering partial: index-only scan over unrouted, non-closed rows.
|
|
96
|
+
// Selects exactly the columns the router reads, eliminating heap fetches per batch.
|
|
97
|
+
await (0, kysely_1.sql) `CREATE INDEX idx_report_unassigned_id ON report (id)
|
|
98
|
+
INCLUDE (status, "reportType", "recordPath", "subjectMessageId")
|
|
99
|
+
WHERE "queueId" IS NULL AND status != 'closed'`.execute(db);
|
|
100
|
+
// Index for report statistics
|
|
101
|
+
await db.schema
|
|
102
|
+
.createIndex('idx_report_queue_created_id')
|
|
103
|
+
.on('report')
|
|
104
|
+
.columns(['queueId', 'createdAt', 'id'])
|
|
105
|
+
.execute();
|
|
106
|
+
// aggregate pending count query
|
|
107
|
+
await (0, kysely_1.sql) `CREATE INDEX idx_report_pending ON report (id) WHERE status != 'closed'`.execute(db);
|
|
108
|
+
// per-queue pending count query
|
|
109
|
+
await (0, kysely_1.sql) `CREATE INDEX idx_report_queue_pending ON report ("queueId") WHERE status != 'closed'`.execute(db);
|
|
110
|
+
// Queue-router event-source partial: scans new modEventReport rows by id
|
|
111
|
+
// for the daemon that inserts report rows from moderation_event.
|
|
112
|
+
await (0, kysely_1.sql) `CREATE INDEX moderation_event_report_id_idx
|
|
113
|
+
ON moderation_event (id)
|
|
114
|
+
WHERE action = 'tools.ozone.moderation.defs#modEventReport'`.execute(db);
|
|
115
|
+
// Stats windowed queries: aggregate/typeWindow filter by createdAt range and
|
|
116
|
+
// include both open and closed reports, so they cannot use the partial indexes
|
|
117
|
+
// above. (createdAt, reportType) ordering serves the date-range scan and
|
|
118
|
+
// satisfies GROUP BY reportType from the index without a heap fetch.
|
|
119
|
+
await (0, kysely_1.sql) `CREATE INDEX idx_report_created_type
|
|
120
|
+
ON report ("createdAt", "reportType")`.execute(db);
|
|
121
|
+
}
|
|
122
|
+
async function down(db) {
|
|
123
|
+
await db.schema.dropIndex('moderation_event_report_id_idx').execute();
|
|
124
|
+
await db.schema.dropTable('report').execute();
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=20260219T164523000Z-create-report-table.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260219T164523000Z-create-report-table.js","sourceRoot":"","sources":["../../../src/db/migrations/20260219T164523000Z-create-report-table.ts"],"names":[],"mappings":"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","sourcesContent":["import { Kysely, sql } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n // Report table - bridges report events to action events\n await db.schema\n .createTable('report')\n .addColumn('id', 'serial', (col) => col.primaryKey())\n\n // Core link to report event (display data still comes from moderation_event via JOIN)\n .addColumn('eventId', 'integer', (col) => col.notNull().unique())\n\n // Queue assignment (computed by background job in future iteration)\n .addColumn('queueId', 'integer') // NULL = not yet assigned, -1 = no matching queue\n .addColumn('queuedAt', 'varchar')\n\n // Action linkage (sorted DESC, most recent first)\n .addColumn('actionEventIds', 'jsonb') // Array of event IDs: [newest_id, ..., oldest_id]\n\n // Reporter communication\n .addColumn('actionNote', 'text')\n\n // Whether the report is muted (reporter was muted or subject was muted at creation time)\n .addColumn('isMuted', 'boolean', (col) => col.notNull().defaultTo(false))\n\n // Status of the ticket/report\n .addColumn('status', 'varchar', (col) => col.notNull().defaultTo('open')) // \"open\", \"closed\", \"escalated\"\n\n // Denormalized from moderation_event for filtering without JOIN\n .addColumn('reportType', 'varchar', (col) => col.notNull())\n .addColumn('did', 'varchar', (col) => col.notNull())\n .addColumn('recordPath', 'varchar', (col) => col.notNull().defaultTo('')) // '' = account/message, 'collection/rkey' = record\n .addColumn('subjectMessageId', 'varchar') // NULL for non-message subjects\n\n // Timestamps\n .addColumn('createdAt', 'varchar', (col) => col.notNull())\n .addColumn('updatedAt', 'varchar', (col) => col.notNull())\n .addColumn('assignedTo', 'varchar') // DID of permanently assigned moderator\n .addColumn('assignedAt', 'varchar') // When the permanent assignment was created\n .addColumn('closedAt', 'varchar')\n .execute()\n\n // ─── Indexes ───\n // Primary JOIN index - critical for every query that fetches display data from moderation_event\n await db.schema\n .createIndex('idx_report_event')\n .on('report')\n .column('eventId')\n .execute()\n\n // ─── Hot path: active reports (status != 'closed') ───\n // Partial filter keeps these tight even as closed reports accumulate (~90% of table long-term).\n // No isMuted in key (low cardinality, rarely filtered) and no INCLUDE columns\n // (display data comes from moderation_event JOIN anyway).\n\n // queryReports: queueId + status, paginated by createdAt\n await sql`CREATE INDEX idx_report_active_queue_created ON report\n (\"queueId\", status, \"createdAt\" DESC, id DESC)\n WHERE status != 'closed'`.execute(db)\n\n // queryReports: queueId + status, paginated by updatedAt\n await sql`CREATE INDEX idx_report_active_queue_updated ON report\n (\"queueId\", status, \"updatedAt\" DESC, id DESC)\n WHERE status != 'closed'`.execute(db)\n\n // queryReports: status only, paginated by createdAt\n await sql`CREATE INDEX idx_report_active_status_created ON report\n (status, \"createdAt\" DESC, id DESC)\n WHERE status != 'closed'`.execute(db)\n\n // queryReports: status only, paginated by updatedAt\n await sql`CREATE INDEX idx_report_active_status_updated ON report\n (status, \"updatedAt\" DESC, id DESC)\n WHERE status != 'closed'`.execute(db)\n\n // Active reports for a specific account (with optional queueId post-filter)\n await sql`CREATE INDEX idx_report_active_did_created ON report\n (did, status, \"createdAt\" DESC, id DESC)\n WHERE status != 'closed'`.execute(db)\n\n // A moderator's active workload (with optional queueId post-filter)\n await sql`CREATE INDEX idx_report_active_assigned_created ON report\n (\"assignedTo\", status, \"createdAt\" DESC, id DESC)\n WHERE status != 'closed'`.execute(db)\n\n // findReportsForSubject hot path — always filters NOT IN ('closed').\n // did + recordPath identify the subject (account or specific record).\n await sql`CREATE INDEX idx_report_subject_active ON report\n (did, \"recordPath\", \"createdAt\" DESC, id DESC)\n WHERE status != 'closed'`.execute(db)\n\n // ─── Closed history (status = 'closed') ───\n // Closed reports are terminal; only sort by createdAt.\n\n // Closed pagination per queue\n await sql`CREATE INDEX idx_report_closed_queue_created ON report\n (\"queueId\", \"createdAt\" DESC, id DESC)\n WHERE status = 'closed'`.execute(db)\n\n // Closed history for an account\n await sql`CREATE INDEX idx_report_closed_did_created ON report\n (did, \"createdAt\" DESC, id DESC)\n WHERE status = 'closed'`.execute(db)\n\n // Moderator's closed-report history\n await sql`CREATE INDEX idx_report_closed_assigned_created ON report\n (\"assignedTo\", \"createdAt\" DESC, id DESC)\n WHERE status = 'closed'`.execute(db)\n\n // ─── Other access patterns ───\n\n // Collection prefix queries: left-anchored LIKE 'app.bsky.feed.post/%' or 'app.bsky.%'\n // text_pattern_ops enables btree-scannable prefix matching (supported since Postgres 8.x)\n await sql`CREATE INDEX idx_report_record_path_pattern ON report\n (\"recordPath\" text_pattern_ops)`.execute(db)\n\n // Queue-router covering partial: index-only scan over unrouted, non-closed rows.\n // Selects exactly the columns the router reads, eliminating heap fetches per batch.\n await sql`CREATE INDEX idx_report_unassigned_id ON report (id)\n INCLUDE (status, \"reportType\", \"recordPath\", \"subjectMessageId\")\n WHERE \"queueId\" IS NULL AND status != 'closed'`.execute(db)\n\n // Index for report statistics\n await db.schema\n .createIndex('idx_report_queue_created_id')\n .on('report')\n .columns(['queueId', 'createdAt', 'id'])\n .execute()\n\n // aggregate pending count query\n await sql`CREATE INDEX idx_report_pending ON report (id) WHERE status != 'closed'`.execute(\n db,\n )\n // per-queue pending count query\n await sql`CREATE INDEX idx_report_queue_pending ON report (\"queueId\") WHERE status != 'closed'`.execute(\n db,\n )\n\n // Queue-router event-source partial: scans new modEventReport rows by id\n // for the daemon that inserts report rows from moderation_event.\n await sql`CREATE INDEX moderation_event_report_id_idx\n ON moderation_event (id)\n WHERE action = 'tools.ozone.moderation.defs#modEventReport'`.execute(db)\n\n // Stats windowed queries: aggregate/typeWindow filter by createdAt range and\n // include both open and closed reports, so they cannot use the partial indexes\n // above. (createdAt, reportType) ordering serves the date-range scan and\n // satisfies GROUP BY reportType from the index without a heap fetch.\n await sql`CREATE INDEX idx_report_created_type\n ON report (\"createdAt\", \"reportType\")`.execute(db)\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n await db.schema.dropIndex('moderation_event_report_id_idx').execute()\n await db.schema.dropTable('report').execute()\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260219T165302248Z-moderator-assignment.d.ts","sourceRoot":"","sources":["../../../src/db/migrations/20260219T165302248Z-moderator-assignment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAO,MAAM,QAAQ,CAAA;AAEpC,wBAAsB,EAAE,CAAC,EAAE,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAmC3D;AAED,wBAAsB,IAAI,CAAC,EAAE,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAE7D"}
|