@atproto/ozone 0.1.68 → 0.1.70

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 (135) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/api/moderation/queryStatuses.d.ts.map +1 -1
  3. package/dist/api/moderation/queryStatuses.js +1 -33
  4. package/dist/api/moderation/queryStatuses.js.map +1 -1
  5. package/dist/background.d.ts +49 -6
  6. package/dist/background.d.ts.map +1 -1
  7. package/dist/background.js +149 -14
  8. package/dist/background.js.map +1 -1
  9. package/dist/config/config.d.ts +1 -0
  10. package/dist/config/config.d.ts.map +1 -1
  11. package/dist/config/config.js +1 -0
  12. package/dist/config/config.js.map +1 -1
  13. package/dist/config/env.d.ts +1 -0
  14. package/dist/config/env.d.ts.map +1 -1
  15. package/dist/config/env.js +1 -0
  16. package/dist/config/env.js.map +1 -1
  17. package/dist/daemon/context.d.ts +9 -3
  18. package/dist/daemon/context.d.ts.map +1 -1
  19. package/dist/daemon/context.js +33 -3
  20. package/dist/daemon/context.js.map +1 -1
  21. package/dist/daemon/index.d.ts.map +1 -1
  22. package/dist/daemon/index.js +3 -6
  23. package/dist/daemon/index.js.map +1 -1
  24. package/dist/daemon/materialized-view-refresher.d.ts +5 -0
  25. package/dist/daemon/materialized-view-refresher.d.ts.map +1 -0
  26. package/dist/daemon/materialized-view-refresher.js +29 -0
  27. package/dist/daemon/materialized-view-refresher.js.map +1 -0
  28. package/dist/db/migrations/20241220T144630860Z-stats-materialized-views.d.ts +5 -0
  29. package/dist/db/migrations/20241220T144630860Z-stats-materialized-views.d.ts.map +1 -0
  30. package/dist/db/migrations/20241220T144630860Z-stats-materialized-views.js +158 -0
  31. package/dist/db/migrations/20241220T144630860Z-stats-materialized-views.js.map +1 -0
  32. package/dist/db/migrations/index.d.ts +1 -0
  33. package/dist/db/migrations/index.d.ts.map +1 -1
  34. package/dist/db/migrations/index.js +2 -1
  35. package/dist/db/migrations/index.js.map +1 -1
  36. package/dist/db/schema/account_events_stats.d.ts +15 -0
  37. package/dist/db/schema/account_events_stats.d.ts.map +1 -0
  38. package/dist/db/schema/account_events_stats.js +5 -0
  39. package/dist/db/schema/account_events_stats.js.map +1 -0
  40. package/dist/db/schema/account_record_events_stats.d.ts +15 -0
  41. package/dist/db/schema/account_record_events_stats.d.ts.map +1 -0
  42. package/dist/db/schema/account_record_events_stats.js +5 -0
  43. package/dist/db/schema/account_record_events_stats.js.map +1 -0
  44. package/dist/db/schema/account_record_status_stats.d.ts +15 -0
  45. package/dist/db/schema/account_record_status_stats.d.ts.map +1 -0
  46. package/dist/db/schema/account_record_status_stats.js +5 -0
  47. package/dist/db/schema/account_record_status_stats.js.map +1 -0
  48. package/dist/db/schema/index.d.ts +5 -1
  49. package/dist/db/schema/index.d.ts.map +1 -1
  50. package/dist/db/schema/record_events_stats.d.ts +14 -0
  51. package/dist/db/schema/record_events_stats.d.ts.map +1 -0
  52. package/dist/db/schema/record_events_stats.js +5 -0
  53. package/dist/db/schema/record_events_stats.js.map +1 -0
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +1 -4
  56. package/dist/index.js.map +1 -1
  57. package/dist/lexicon/index.d.ts +2 -0
  58. package/dist/lexicon/index.d.ts.map +1 -1
  59. package/dist/lexicon/index.js +2 -0
  60. package/dist/lexicon/index.js.map +1 -1
  61. package/dist/lexicon/lexicons.d.ts +230 -2
  62. package/dist/lexicon/lexicons.d.ts.map +1 -1
  63. package/dist/lexicon/lexicons.js +126 -1
  64. package/dist/lexicon/lexicons.js.map +1 -1
  65. package/dist/lexicon/types/app/bsky/feed/defs.d.ts +5 -0
  66. package/dist/lexicon/types/app/bsky/feed/defs.d.ts.map +1 -1
  67. package/dist/lexicon/types/app/bsky/feed/defs.js +5 -1
  68. package/dist/lexicon/types/app/bsky/feed/defs.js.map +1 -1
  69. package/dist/lexicon/types/app/bsky/feed/generator.d.ts +1 -0
  70. package/dist/lexicon/types/app/bsky/feed/generator.d.ts.map +1 -1
  71. package/dist/lexicon/types/app/bsky/feed/generator.js.map +1 -1
  72. package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts +40 -0
  73. package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts.map +1 -1
  74. package/dist/lexicon/types/tools/ozone/moderation/defs.js +20 -0
  75. package/dist/lexicon/types/tools/ozone/moderation/defs.js.map +1 -1
  76. package/dist/lexicon/types/tools/ozone/moderation/queryStatuses.d.ts +7 -1
  77. package/dist/lexicon/types/tools/ozone/moderation/queryStatuses.d.ts.map +1 -1
  78. package/dist/mod-service/index.d.ts +4 -62
  79. package/dist/mod-service/index.d.ts.map +1 -1
  80. package/dist/mod-service/index.js +80 -74
  81. package/dist/mod-service/index.js.map +1 -1
  82. package/dist/mod-service/status.d.ts +115 -4
  83. package/dist/mod-service/status.d.ts.map +1 -1
  84. package/dist/mod-service/status.js +51 -34
  85. package/dist/mod-service/status.js.map +1 -1
  86. package/dist/mod-service/types.d.ts +16 -1
  87. package/dist/mod-service/types.d.ts.map +1 -1
  88. package/dist/mod-service/views.d.ts.map +1 -1
  89. package/dist/mod-service/views.js +49 -41
  90. package/dist/mod-service/views.js.map +1 -1
  91. package/dist/util.d.ts +34 -0
  92. package/dist/util.d.ts.map +1 -1
  93. package/dist/util.js +132 -0
  94. package/dist/util.js.map +1 -1
  95. package/package.json +3 -3
  96. package/src/api/moderation/queryStatuses.ts +1 -63
  97. package/src/background.ts +140 -14
  98. package/src/config/config.ts +2 -0
  99. package/src/config/env.ts +4 -0
  100. package/src/daemon/context.ts +43 -5
  101. package/src/daemon/index.ts +3 -6
  102. package/src/daemon/materialized-view-refresher.ts +27 -0
  103. package/src/db/migrations/20241220T144630860Z-stats-materialized-views.ts +218 -0
  104. package/src/db/migrations/index.ts +1 -0
  105. package/src/db/schema/account_events_stats.ts +16 -0
  106. package/src/db/schema/account_record_events_stats.ts +15 -0
  107. package/src/db/schema/account_record_status_stats.ts +15 -0
  108. package/src/db/schema/index.ts +10 -1
  109. package/src/db/schema/record_events_stats.ts +15 -0
  110. package/src/index.ts +1 -7
  111. package/src/lexicon/index.ts +2 -0
  112. package/src/lexicon/lexicons.ts +138 -1
  113. package/src/lexicon/types/app/bsky/feed/defs.ts +9 -0
  114. package/src/lexicon/types/app/bsky/feed/generator.ts +4 -0
  115. package/src/lexicon/types/tools/ozone/moderation/defs.ts +62 -0
  116. package/src/lexicon/types/tools/ozone/moderation/queryStatuses.ts +11 -1
  117. package/src/mod-service/index.ts +181 -118
  118. package/src/mod-service/status.ts +55 -28
  119. package/src/mod-service/types.ts +22 -1
  120. package/src/mod-service/views.ts +64 -50
  121. package/src/util.ts +145 -0
  122. package/tests/__snapshots__/get-record.test.ts.snap +28 -0
  123. package/tests/__snapshots__/get-records.test.ts.snap +14 -0
  124. package/tests/__snapshots__/get-repo.test.ts.snap +11 -0
  125. package/tests/__snapshots__/get-repos.test.ts.snap +11 -0
  126. package/tests/__snapshots__/moderation-events.test.ts.snap +19 -0
  127. package/tests/__snapshots__/moderation-statuses.test.ts.snap +114 -0
  128. package/tests/get-record.test.ts +4 -0
  129. package/tests/get-records.test.ts +4 -0
  130. package/tests/get-repo.test.ts +4 -0
  131. package/tests/get-repos.test.ts +4 -0
  132. package/tests/moderation-events.test.ts +4 -0
  133. package/tests/moderation-statuses.test.ts +4 -0
  134. package/tests/query-labels.test.ts +1 -0
  135. package/tsconfig.build.tsbuildinfo +1 -1
package/dist/util.js CHANGED
@@ -1,6 +1,13 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.formatLabelerHeader = exports.defaultLabelerHeader = exports.parseLabelerHeader = exports.LABELER_HEADER_NAME = exports.retryHttp = exports.RETRYABLE_HTTP_STATUS_CODES = exports.getSigningKeyId = void 0;
7
+ exports.startInterval = startInterval;
8
+ exports.isCausedBySignal = isCausedBySignal;
9
+ exports.boundAbortController = boundAbortController;
10
+ const node_assert_1 = __importDefault(require("node:assert"));
4
11
  const common_1 = require("@atproto/common");
5
12
  const xrpc_1 = require("@atproto/xrpc");
6
13
  const structured_headers_1 = require("structured-headers");
@@ -71,4 +78,129 @@ const formatLabelerHeader = (parsed) => {
71
78
  return parts.join(',');
72
79
  };
73
80
  exports.formatLabelerHeader = formatLabelerHeader;
81
+ /**
82
+ * Utility function similar to `setInterval()`. The main difference is that the
83
+ * execution is controlled through a signal and that the function will wait for
84
+ * `interval` milliseconds *between* the end of the previous execution and the
85
+ * start of the next one (instead of starting the execution every `interval`
86
+ * milliseconds), ensuring that the function is not running concurrently.
87
+ *
88
+ * @param fn The function to execute. That function must not throw any error
89
+ * other than {@link signal}'s {@link AbortSignal.reason} or an {@link Error}
90
+ * that has the {@link signal}'s {@link AbortSignal.reason} as its
91
+ * {@link Error.cause}.
92
+ *
93
+ * @returns A promise that resolves when the signal is aborted, and the last
94
+ * execution is done.
95
+ *
96
+ * @throws {AbortSignal['reason']} if the {@link signal} is already aborted.
97
+ * @throws {TypeError} if {@link fn} throws an unexpected error (with the
98
+ * unexpected error as the {@link Error.cause}).
99
+ */
100
+ async function startInterval(fn, interval, signal, runImmediately = false) {
101
+ signal.throwIfAborted();
102
+ // Renaming for clarity
103
+ const inputSignal = signal;
104
+ const intervalController = new AbortController();
105
+ const intervalSignal = intervalController.signal;
106
+ return new Promise((resolve, reject) => {
107
+ let timer;
108
+ const run = async () => {
109
+ // Cloning the signal for this particular run to prevent memory leaks
110
+ const runController = boundAbortController(intervalSignal);
111
+ const runSignal = runController.signal;
112
+ try {
113
+ await fn(runSignal);
114
+ }
115
+ catch (err) {
116
+ if (err != null && isCausedBySignal(err, runSignal)) {
117
+ // Silently ignore the error if it is caused by the signal. At this
118
+ // point, the interval controller was aborted, which will cause the
119
+ // promise to resolve in the "finally" block bellow.
120
+ }
121
+ else {
122
+ // Invalid behavior: stop the interval and reject the promise.
123
+ const error = new TypeError('Unexpected error', { cause: err });
124
+ // Rejecting here will make `resolve()` in the "finally" block to be a
125
+ // no-op. Rejecting before aborting the controller to ensure the
126
+ // promise does not get resolved by the `abort` event listeners.
127
+ reject(error);
128
+ // Using `error` as abort reason to avoid creating an AbortError.
129
+ intervalController.abort(error);
130
+ }
131
+ }
132
+ finally {
133
+ // Cleanup the listeners added by `boundAbortController`
134
+ runController.abort();
135
+ if (intervalSignal.aborted)
136
+ resolve();
137
+ else
138
+ schedule();
139
+ }
140
+ };
141
+ const schedule = () => {
142
+ (0, node_assert_1.default)(timer === undefined, 'unexpected state');
143
+ timer = setTimeout(() => {
144
+ timer = undefined; // "running" state
145
+ void run();
146
+ }, interval);
147
+ };
148
+ inputSignal.addEventListener('abort',
149
+ // This function will only be called if the `inputSignal` is aborted
150
+ // before the interval controller is aborted.
151
+ () => {
152
+ // Stop the interval, using the input signal's reason
153
+ intervalController.abort(inputSignal.reason);
154
+ if (timer === undefined) {
155
+ // `fn` is currently running; `run`'s finally block will resolve the
156
+ // promise.
157
+ }
158
+ else {
159
+ // The execution was scheduled but not started yet. Clear the timer
160
+ // and resolve the promise.
161
+ clearTimeout(timer);
162
+ resolve();
163
+ }
164
+ },
165
+ // Remove the listener whenever the interval is aborted.
166
+ { signal: intervalSignal });
167
+ if (runImmediately)
168
+ void run();
169
+ else
170
+ schedule();
171
+ });
172
+ }
173
+ /**
174
+ * Determines whether the cause of an error is a signal's reason
175
+ */
176
+ function isCausedBySignal(err, signal) {
177
+ if (!signal.aborted)
178
+ return false;
179
+ if (signal.reason == null)
180
+ return false; // Ignore nullish reasons
181
+ return (err === signal.reason ||
182
+ (err instanceof Error && err.cause === signal.reason));
183
+ }
184
+ /**
185
+ * Creates an AbortController that will be aborted when any of the given signals
186
+ * is aborted.
187
+ *
188
+ * @note Make sure to call `abortController.abort()` when you are done with
189
+ * the controller to avoid memory leaks.
190
+ *
191
+ * @throws if any of the input signals is already aborted.
192
+ */
193
+ function boundAbortController(...signals) {
194
+ for (const signal of signals) {
195
+ signal?.throwIfAborted();
196
+ }
197
+ const abortController = new AbortController();
198
+ const abort = function (event) {
199
+ abortController.abort(event.target?.reason);
200
+ };
201
+ for (const signal of signals) {
202
+ signal?.addEventListener('abort', abort, { signal: abortController.signal });
203
+ }
204
+ return abortController;
205
+ }
74
206
  //# sourceMappingURL=util.js.map
package/dist/util.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/ozone",
3
- "version": "0.1.68",
3
+ "version": "0.1.70",
4
4
  "license": "MIT",
5
5
  "description": "Backend service for moderating the Bluesky network.",
6
6
  "keywords": [
@@ -32,7 +32,7 @@
32
32
  "typed-emitter": "^2.1.0",
33
33
  "uint8arrays": "3.0.0",
34
34
  "undici": "^6.14.1",
35
- "@atproto/api": "^0.13.27",
35
+ "@atproto/api": "^0.13.29",
36
36
  "@atproto/common": "^0.4.6",
37
37
  "@atproto/crypto": "^0.4.3",
38
38
  "@atproto/identity": "^0.4.5",
@@ -52,7 +52,7 @@
52
52
  "ts-node": "^10.8.2",
53
53
  "typescript": "^5.6.3",
54
54
  "@atproto/lex-cli": "^0.5.5",
55
- "@atproto/pds": "^0.4.85"
55
+ "@atproto/pds": "^0.4.87"
56
56
  },
57
57
  "scripts": {
58
58
  "codegen": "lex gen-server --yes ./src/lexicon ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/* ../../lexicons/chat/bsky/*/* ../../lexicons/tools/ozone/*/*",
@@ -1,75 +1,13 @@
1
1
  import { Server } from '../../lexicon'
2
2
  import AppContext from '../../context'
3
- import { getReviewState } from '../util'
4
3
 
5
4
  export default function (server: Server, ctx: AppContext) {
6
5
  server.tools.ozone.moderation.queryStatuses({
7
6
  auth: ctx.authVerifier.modOrAdminToken,
8
7
  handler: async ({ params }) => {
9
- const {
10
- includeAllUserRecords,
11
- subject,
12
- takendown,
13
- appealed,
14
- reviewState,
15
- reviewedAfter,
16
- reviewedBefore,
17
- reportedAfter,
18
- reportedBefore,
19
- ignoreSubjects,
20
- lastReviewedBy,
21
- hostingDeletedBefore,
22
- hostingDeletedAfter,
23
- hostingUpdatedBefore,
24
- hostingUpdatedAfter,
25
- hostingStatuses,
26
- sortDirection = 'desc',
27
- sortField = 'lastReportedAt',
28
- includeMuted = false,
29
- onlyMuted = false,
30
- limit = 50,
31
- cursor,
32
- tags = [],
33
- excludeTags = [],
34
- collections = [],
35
- subjectType,
36
- queueCount,
37
- queueIndex,
38
- queueSeed,
39
- } = params
40
8
  const db = ctx.db
41
9
  const modService = ctx.modService(db)
42
- const results = await modService.getSubjectStatuses({
43
- reviewState: getReviewState(reviewState),
44
- includeAllUserRecords,
45
- subject,
46
- takendown,
47
- appealed,
48
- reviewedAfter,
49
- reviewedBefore,
50
- reportedAfter,
51
- reportedBefore,
52
- includeMuted,
53
- hostingDeletedBefore,
54
- hostingDeletedAfter,
55
- hostingUpdatedBefore,
56
- hostingUpdatedAfter,
57
- hostingStatuses,
58
- onlyMuted,
59
- ignoreSubjects,
60
- sortDirection,
61
- lastReviewedBy,
62
- sortField,
63
- limit,
64
- cursor,
65
- tags,
66
- excludeTags,
67
- collections,
68
- subjectType,
69
- queueCount,
70
- queueIndex,
71
- queueSeed,
72
- })
10
+ const results = await modService.getSubjectStatuses(params)
73
11
  const subjectStatuses = results.statuses.map((status) =>
74
12
  modService.views.formatSubjectStatus(status),
75
13
  )
package/src/background.ts CHANGED
@@ -1,35 +1,161 @@
1
1
  import PQueue from 'p-queue'
2
2
  import { Database } from './db'
3
3
  import { dbLogger } from './logger'
4
+ import { boundAbortController, isCausedBySignal, startInterval } from './util'
4
5
 
5
- // A simple queue for in-process, out-of-band/backgrounded work
6
+ type Task = (db: Database, signal: AbortSignal) => Promise<void>
6
7
 
8
+ /**
9
+ * A simple queue for in-process, out-of-band/backgrounded work
10
+ */
7
11
  export class BackgroundQueue {
8
- queue = new PQueue({ concurrency: 20 })
9
- destroyed = false
10
- constructor(public db: Database) {}
12
+ private abortController = new AbortController()
13
+ private queue = new PQueue({ concurrency: 20 })
11
14
 
12
- add(task: Task) {
15
+ public get signal() {
16
+ return this.abortController.signal
17
+ }
18
+
19
+ public get destroyed() {
20
+ return this.signal.aborted
21
+ }
22
+
23
+ constructor(protected db: Database) {}
24
+
25
+ getStats() {
26
+ return {
27
+ runningCount: this.queue.pending,
28
+ waitingCount: this.queue.size,
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Add a task that will be executed at some point in the future.
34
+ *
35
+ * The task will be executed even if the backgroundQueue is destroyed, unless
36
+ * the provided `signal` is aborted.
37
+ *
38
+ * The `signal` provided to the task will be aborted whenever either the
39
+ * backgroundQueue is destroyed or the provided `signal` is aborted.
40
+ */
41
+ async add(task: Task, signal?: AbortSignal): Promise<void> {
13
42
  if (this.destroyed) {
14
43
  return
15
44
  }
16
- this.queue
17
- .add(() => task(this.db))
18
- .catch((err) => {
19
- dbLogger.error(err, 'background queue task failed')
20
- })
45
+
46
+ const abortController = boundAbortController(this.signal, signal)
47
+
48
+ return this.queue.add<void>(async () => {
49
+ try {
50
+ // Do not run the task if the signal provided to the task has become
51
+ // aborted. Do not use `abortController.signal` here since we do not
52
+ // want to abort the task if the backgroundQueue is being destroyed.
53
+ if (signal?.aborted) return
54
+
55
+ // The task will receive a "combined signal" allowing it to abort if
56
+ // either the backgroundQueue is destroyed or the provided signal is
57
+ // aborted.
58
+ await task(this.db, abortController.signal)
59
+ } catch (err) {
60
+ if (!isCausedBySignal(err, abortController.signal)) {
61
+ dbLogger.error(err, 'background queue task failed')
62
+ }
63
+ } finally {
64
+ abortController.abort()
65
+ }
66
+ })
21
67
  }
22
68
 
23
69
  async processAll() {
24
70
  await this.queue.onIdle()
25
71
  }
26
72
 
27
- // On destroy we stop accepting new tasks, but complete all pending/in-progress tasks.
28
- // The application calls this only once http connections have drained (tasks no longer being added).
73
+ /**
74
+ * On destroy we stop accepting new tasks, but complete all
75
+ * pending/in-progress tasks. Tasks can decide to abort their current
76
+ * operation based on the signal they received. The application calls this
77
+ * only once http connections have drained (tasks no longer being added).
78
+ */
29
79
  async destroy() {
30
- this.destroyed = true
80
+ this.abortController.abort()
31
81
  await this.queue.onIdle()
32
82
  }
33
83
  }
34
84
 
35
- type Task = (db: Database) => Promise<void>
85
+ /**
86
+ * A simple periodic background task runner. This class will schedule a task to
87
+ * run through a provided {@link BackgroundQueue} at a fixed interval. The task
88
+ * will never run more than once concurrently, and will wait at least `interval`
89
+ * milliseconds between the end of one run and the start of the next.
90
+ */
91
+ export class PeriodicBackgroundTask {
92
+ private abortController: AbortController
93
+
94
+ private intervalPromise?: Promise<void>
95
+ private runningPromise?: Promise<void>
96
+
97
+ public get signal() {
98
+ return this.abortController.signal
99
+ }
100
+
101
+ public get destroyed() {
102
+ return this.signal.aborted
103
+ }
104
+
105
+ constructor(
106
+ protected backgroundQueue: BackgroundQueue,
107
+ protected interval: number,
108
+ protected task: Task,
109
+ ) {
110
+ if (!Number.isFinite(interval) || interval <= 0) {
111
+ throw new TypeError('interval must be a positive number')
112
+ }
113
+
114
+ // Bind this class's signal to the backgroundQueue's signal (destroying this
115
+ // instance if the backgroundQueue is destroyed)
116
+ this.abortController = boundAbortController(backgroundQueue.signal)
117
+ }
118
+
119
+ public run(signal?: AbortSignal): Promise<void> {
120
+ // `startInterval` already ensures that only one run is in progress at a
121
+ // time. However, we want to be able to expose a `run()` method that can be
122
+ // used to force a run, which could cause concurrent executions. We prevent
123
+ // this using the `runningPromise` property.
124
+
125
+ if (this.runningPromise) return this.runningPromise
126
+
127
+ // Combine the `this.signal` with the provided `signal`, if any.
128
+ const abortController = boundAbortController(this.signal, signal)
129
+
130
+ const promise = this.backgroundQueue.add(this.task, abortController.signal)
131
+
132
+ return (this.runningPromise = promise).finally(() => {
133
+ if (this.runningPromise === promise) this.runningPromise = undefined
134
+
135
+ // Cleanup the listeners added by `boundAbortController`
136
+ abortController.abort()
137
+ })
138
+ }
139
+
140
+ public start() {
141
+ // Noop if already started. Throws if this.signal is aborted (instance is
142
+ // destroyed).
143
+ this.intervalPromise ||= startInterval(
144
+ async (signal) => this.run(signal),
145
+ this.interval,
146
+ this.signal,
147
+ )
148
+ }
149
+
150
+ public async destroy() {
151
+ // @NOTE This instance does not "own" the backgroundQueue, so we do not
152
+ // destroy it here.
153
+
154
+ this.abortController.abort()
155
+ console.error('ABOOOORT')
156
+
157
+ await this.intervalPromise
158
+ this.intervalPromise = undefined
159
+ console.error('DONE -_-')
160
+ }
161
+ }
@@ -24,6 +24,7 @@ export const envToCfg = (env: OzoneEnvironment): OzoneConfig => {
24
24
  poolSize: env.dbPoolSize,
25
25
  poolMaxUses: env.dbPoolMaxUses,
26
26
  poolIdleTimeoutMs: env.dbPoolIdleTimeoutMs,
27
+ materializedViewRefreshIntervalMs: env.dbMaterializedViewRefreshIntervalMs,
27
28
  }
28
29
 
29
30
  assert(env.appviewUrl, 'appviewUrl is required')
@@ -122,6 +123,7 @@ export type DatabaseConfig = {
122
123
  poolSize?: number
123
124
  poolMaxUses?: number
124
125
  poolIdleTimeoutMs?: number
126
+ materializedViewRefreshIntervalMs?: number
125
127
  }
126
128
 
127
129
  export type AppviewConfig = {
package/src/config/env.ts CHANGED
@@ -20,6 +20,9 @@ export const readEnv = (): OzoneEnvironment => {
20
20
  dbPoolSize: envInt('OZONE_DB_POOL_SIZE'),
21
21
  dbPoolMaxUses: envInt('OZONE_DB_POOL_MAX_USES'),
22
22
  dbPoolIdleTimeoutMs: envInt('OZONE_DB_POOL_IDLE_TIMEOUT_MS'),
23
+ dbMaterializedViewRefreshIntervalMs: envInt(
24
+ 'OZONE_DB_MATERIALIZED_VIEW_REFRESH_INTERVAL_MS',
25
+ ),
23
26
  didPlcUrl: envStr('OZONE_DID_PLC_URL'),
24
27
  didCacheStaleTTL: envInt('OZONE_DID_CACHE_STALE_TTL'),
25
28
  didCacheMaxTTL: envInt('OZONE_DID_CACHE_MAX_TTL'),
@@ -53,6 +56,7 @@ export type OzoneEnvironment = {
53
56
  dbPoolSize?: number
54
57
  dbPoolMaxUses?: number
55
58
  dbPoolIdleTimeoutMs?: number
59
+ dbMaterializedViewRefreshIntervalMs?: number
56
60
  didPlcUrl?: string
57
61
  didCacheStaleTTL?: number
58
62
  didCacheMaxTTL?: number
@@ -6,17 +6,20 @@ import { OzoneConfig, OzoneSecrets } from '../config'
6
6
  import { Database } from '../db'
7
7
  import { EventPusher } from './event-pusher'
8
8
  import { EventReverser } from './event-reverser'
9
- import { ModerationService, ModerationServiceCreator } from '../mod-service'
9
+ import { ModerationService } from '../mod-service'
10
10
  import { BackgroundQueue } from '../background'
11
11
  import { getSigningKeyId } from '../util'
12
+ import { MaterializedViewRefresher } from './materialized-view-refresher'
13
+ import { allFulfilled } from '@atproto/common'
12
14
 
13
15
  export type DaemonContextOptions = {
14
16
  db: Database
15
17
  cfg: OzoneConfig
16
- modService: ModerationServiceCreator
18
+ backgroundQueue: BackgroundQueue
17
19
  signingKey: Keypair
18
20
  eventPusher: EventPusher
19
21
  eventReverser: EventReverser
22
+ materializedViewRefresher: MaterializedViewRefresher
20
23
  }
21
24
 
22
25
  export class DaemonContext {
@@ -67,13 +70,19 @@ export class DaemonContext {
67
70
 
68
71
  const eventReverser = new EventReverser(db, modService)
69
72
 
73
+ const materializedViewRefresher = new MaterializedViewRefresher(
74
+ backgroundQueue,
75
+ cfg.db.materializedViewRefreshIntervalMs,
76
+ )
77
+
70
78
  return new DaemonContext({
71
79
  db,
72
80
  cfg,
73
- modService,
81
+ backgroundQueue,
74
82
  signingKey,
75
83
  eventPusher,
76
84
  eventReverser,
85
+ materializedViewRefresher,
77
86
  ...(overrides ?? {}),
78
87
  })
79
88
  }
@@ -86,8 +95,8 @@ export class DaemonContext {
86
95
  return this.opts.cfg
87
96
  }
88
97
 
89
- get modService(): ModerationServiceCreator {
90
- return this.opts.modService
98
+ get backgroundQueue(): BackgroundQueue {
99
+ return this.opts.backgroundQueue
91
100
  }
92
101
 
93
102
  get eventPusher(): EventPusher {
@@ -97,6 +106,35 @@ export class DaemonContext {
97
106
  get eventReverser(): EventReverser {
98
107
  return this.opts.eventReverser
99
108
  }
109
+
110
+ get materializedViewRefresher(): MaterializedViewRefresher {
111
+ return this.opts.materializedViewRefresher
112
+ }
113
+
114
+ async start() {
115
+ this.eventPusher.start()
116
+ this.eventReverser.start()
117
+ this.materializedViewRefresher.start()
118
+ }
119
+
120
+ async processAll() {
121
+ // Sequential because the materialized view values depend on the events.
122
+ await this.eventPusher.processAll()
123
+ await this.materializedViewRefresher.run()
124
+ }
125
+
126
+ async destroy() {
127
+ try {
128
+ await allFulfilled([
129
+ this.eventReverser.destroy(),
130
+ this.eventPusher.destroy(),
131
+ this.materializedViewRefresher.destroy(),
132
+ ])
133
+ } finally {
134
+ await this.backgroundQueue.destroy()
135
+ await this.db.close()
136
+ }
137
+ }
100
138
  }
101
139
 
102
140
  export default DaemonContext
@@ -18,17 +18,14 @@ export class OzoneDaemon {
18
18
  }
19
19
 
20
20
  async start() {
21
- this.ctx.eventPusher.start()
22
- this.ctx.eventReverser.start()
21
+ await this.ctx.start()
23
22
  }
24
23
 
25
24
  async processAll() {
26
- await this.ctx.eventPusher.processAll()
25
+ await this.ctx.processAll()
27
26
  }
28
27
 
29
28
  async destroy() {
30
- await this.ctx.eventReverser.destroy()
31
- await this.ctx.eventPusher.destroy()
32
- await this.ctx.db.close()
29
+ await this.ctx.destroy()
33
30
  }
34
31
  }
@@ -0,0 +1,27 @@
1
+ import { MINUTE } from '@atproto/common'
2
+ import { sql } from 'kysely'
3
+ import { BackgroundQueue, PeriodicBackgroundTask } from '../background'
4
+
5
+ export class MaterializedViewRefresher extends PeriodicBackgroundTask {
6
+ constructor(backgroundQueue: BackgroundQueue, interval = 30 * MINUTE) {
7
+ super(backgroundQueue, interval, async ({ db }, signal) => {
8
+ for (const view of [
9
+ 'account_events_stats',
10
+ 'record_events_stats',
11
+ 'account_record_events_stats',
12
+ 'account_record_status_stats',
13
+ ]) {
14
+ if (signal.aborted) break
15
+
16
+ // Kysely does not provide a way to cancel a running query. Because of
17
+ // this, killing the process during a refresh will cause the process to
18
+ // wait for the current refresh to finish before exiting. This is not
19
+ // ideal, but it is the best we can do until Kysely provides a way to
20
+ // cancel a query.
21
+ await sql`REFRESH MATERIALIZED VIEW CONCURRENTLY ${sql.id(view)}`.execute(
22
+ db,
23
+ )
24
+ }
25
+ })
26
+ }
27
+ }