@superinterface/server 1.0.43 → 1.0.44

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 (28) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +2 -2
  3. package/.next/cache/.tsbuildinfo +1 -1
  4. package/.next/cache/eslint/.cache_btwyo7 +1 -1
  5. package/.next/fallback-build-manifest.json +2 -2
  6. package/.next/server/app/_not-found.html +1 -1
  7. package/.next/server/app/_not-found.rsc +1 -1
  8. package/.next/server/app/index.html +1 -1
  9. package/.next/server/app/index.rsc +1 -1
  10. package/.next/server/chunks/[root-of-the-server]__3a25f63d._.js +3 -3
  11. package/.next/server/chunks/[root-of-the-server]__3a25f63d._.js.map +1 -1
  12. package/.next/server/chunks/[root-of-the-server]__4d829477._.js +3 -3
  13. package/.next/server/chunks/[root-of-the-server]__4d829477._.js.map +1 -1
  14. package/.next/server/chunks/[root-of-the-server]__6169c901._.js +3 -3
  15. package/.next/server/chunks/[root-of-the-server]__6169c901._.js.map +1 -1
  16. package/.next/server/pages/404.html +1 -1
  17. package/.next/server/pages/500.html +1 -1
  18. package/.next/trace +1 -1
  19. package/dist/lib/tasks/getScheduleOccurrences.d.ts +10 -0
  20. package/dist/lib/tasks/getScheduleOccurrences.d.ts.map +1 -0
  21. package/dist/lib/tasks/getScheduleOccurrences.js +301 -0
  22. package/dist/lib/tasks/getTaskScheduleConflict.d.ts.map +1 -1
  23. package/dist/lib/tasks/getTaskScheduleConflict.js +20 -27
  24. package/package.json +1 -1
  25. package/scripts/cleanupTaskConflicts.ts +95 -31
  26. /package/.next/static/{iw7IvcIaTYMhd4DuD49Bv → DQromUo_Z941kWJSSM2pu}/_buildManifest.js +0 -0
  27. /package/.next/static/{iw7IvcIaTYMhd4DuD49Bv → DQromUo_Z941kWJSSM2pu}/_clientMiddlewareManifest.json +0 -0
  28. /package/.next/static/{iw7IvcIaTYMhd4DuD49Bv → DQromUo_Z941kWJSSM2pu}/_ssgManifest.js +0 -0
@@ -3,6 +3,7 @@ import utc from 'dayjs/plugin/utc'
3
3
  import { PrismaClient } from '@prisma/client'
4
4
  import { qstash } from '@/lib/upstash/qstash'
5
5
  import { FIFTEEN_MINUTES_IN_MS } from '@/lib/tasks/getTaskScheduleConflict'
6
+ import { getScheduleOccurrences } from '@/lib/tasks/getScheduleOccurrences'
6
7
  import { TaskScheduleConflictError } from '@/lib/errors'
7
8
  import { loadPrismaClient } from './utils/loadPrisma'
8
9
 
@@ -25,12 +26,8 @@ type TaskWithDates = Awaited<
25
26
  ReturnType<PrismaClient['task']['findMany']>
26
27
  >[number]
27
28
 
28
- const getScheduleStart = (task: TaskWithDates) => {
29
- const start = (task.schedule as { start?: unknown } | null)?.start
30
- if (typeof start !== 'string') return null
31
- const parsed = dayjs(start).utc()
32
- return parsed.isValid() ? parsed : null
33
- }
29
+ const LOOKAHEAD_DAYS = 365
30
+ const MAX_OCCURRENCES = 120
34
31
 
35
32
  const getLastTouchedAt = (task: TaskWithDates) => {
36
33
  const updated = dayjs(task.updatedAt).utc()
@@ -85,6 +82,15 @@ const main = async () => {
85
82
  const options = parseCliOptions()
86
83
  const dryRunLabel = options.dryRun ? '[DRY RUN]' : '[APPLY]'
87
84
 
85
+ if (!options.dryRun && !process.env.QSTASH_TOKEN) {
86
+ console.error(
87
+ `${dryRunLabel} Missing required environment variable QSTASH_TOKEN. Aborting.`,
88
+ )
89
+ await prisma.$disconnect()
90
+ process.exitCode = 1
91
+ return
92
+ }
93
+
88
94
  console.log(
89
95
  `${dryRunLabel} Starting duplicate task audit – ` +
90
96
  `${TaskScheduleConflictError.defaultMessage}`,
@@ -95,6 +101,11 @@ const main = async () => {
95
101
  orderBy: { createdAt: 'asc' },
96
102
  })
97
103
 
104
+ if (!tasks.length) {
105
+ console.log(`${dryRunLabel} No tasks found – nothing to audit.`)
106
+ return
107
+ }
108
+
98
109
  const groups = new Map<string, TaskWithDates[]>()
99
110
  for (const task of tasks) {
100
111
  const key = `${task.threadId}::${task.key}`
@@ -103,35 +114,60 @@ const main = async () => {
103
114
  groups.set(key, list)
104
115
  }
105
116
 
117
+ console.log(
118
+ `${dryRunLabel} Loaded ${tasks.length} task(s) across ${groups.size} group(s).`,
119
+ )
120
+
106
121
  let conflictGroupCount = 0
107
122
  let candidateDeleteCount = 0
123
+ let processedTasks = 0
124
+ let processedGroups = 0
108
125
 
109
126
  for (const [groupKey, taskGroup] of groups.entries()) {
127
+ processedGroups += 1
128
+ processedTasks += taskGroup.length
129
+
130
+ const percentComplete = Math.round((processedTasks / tasks.length) * 100)
131
+ if (
132
+ processedGroups === 1 ||
133
+ processedGroups === groups.size ||
134
+ processedGroups % 10 === 0
135
+ ) {
136
+ console.log(
137
+ `${dryRunLabel} Progress: ${processedTasks}/${tasks.length} task(s) checked (${percentComplete}%).`,
138
+ )
139
+ }
140
+
110
141
  if (taskGroup.length < 2) continue
111
142
 
112
- const sortedByStart = taskGroup
113
- .map((task) => ({
114
- task,
115
- start: getScheduleStart(task),
116
- }))
117
- .sort((a, b) => {
118
- if (!a.start && !b.start) return 0
119
- if (!a.start) return 1
120
- if (!b.start) return -1
121
- return a.start.valueOf() - b.start.valueOf()
122
- })
143
+ const occurrencesMap = new Map<string, dayjs.Dayjs[]>()
123
144
 
124
- const validEntries = sortedByStart
125
- .map((entry, index) => ({ ...entry, index }))
126
- .filter((entry): entry is typeof entry & { start: dayjs.Dayjs } => {
127
- if (!entry.start) {
128
- console.log(
129
- `${dryRunLabel} Skipping task without valid start: ${entry.task.id}`,
130
- )
131
- return false
145
+ const taskEntries = taskGroup
146
+ .map((task) => {
147
+ const occurrences = getScheduleOccurrences(
148
+ task.schedule as PrismaJson.TaskSchedule,
149
+ {
150
+ lookAheadDays: LOOKAHEAD_DAYS,
151
+ maxOccurrences: MAX_OCCURRENCES,
152
+ },
153
+ )
154
+ occurrencesMap.set(task.id, occurrences)
155
+ return {
156
+ task,
157
+ occurrences,
132
158
  }
133
- return true
134
159
  })
160
+ .map((entry, index) => ({ ...entry, index }))
161
+
162
+ const validEntries = taskEntries.filter((entry) => {
163
+ if (!entry.occurrences.length) {
164
+ console.log(
165
+ `${dryRunLabel} Skipping task without upcoming occurrences: ${entry.task.id}`,
166
+ )
167
+ return false
168
+ }
169
+ return true
170
+ })
135
171
 
136
172
  if (validEntries.length < 2) continue
137
173
 
@@ -148,13 +184,28 @@ const main = async () => {
148
184
  parent[rootB] = rootA
149
185
  }
150
186
 
187
+ const hasConflictWithinWindow = (a: dayjs.Dayjs[], b: dayjs.Dayjs[]) => {
188
+ let i = 0
189
+ let j = 0
190
+ while (i < a.length && j < b.length) {
191
+ const diff = a[i].valueOf() - b[j].valueOf()
192
+ if (Math.abs(diff) < FIFTEEN_MINUTES_IN_MS) return true
193
+ if (diff < 0) {
194
+ i += 1
195
+ } else {
196
+ j += 1
197
+ }
198
+ }
199
+ return false
200
+ }
201
+
151
202
  for (let i = 0; i < validEntries.length; i += 1) {
152
203
  const current = validEntries[i]
153
204
  for (let j = i + 1; j < validEntries.length; j += 1) {
154
205
  const compare = validEntries[j]
155
- const diffMs = compare.start.diff(current.start)
156
- if (diffMs >= FIFTEEN_MINUTES_IN_MS) break
157
- if (Math.abs(diffMs) < FIFTEEN_MINUTES_IN_MS) {
206
+ if (
207
+ hasConflictWithinWindow(current.occurrences, compare.occurrences)
208
+ ) {
158
209
  union(i, j)
159
210
  }
160
211
  }
@@ -180,7 +231,7 @@ const main = async () => {
180
231
  for (const cluster of overlapping) {
181
232
  const evaluatedCluster = cluster.map((task) => ({
182
233
  task,
183
- start: getScheduleStart(task),
234
+ occurrences: occurrencesMap.get(task.id) ?? [],
184
235
  lastTouchedAt: getLastTouchedAt(task),
185
236
  }))
186
237
 
@@ -203,9 +254,15 @@ const main = async () => {
203
254
  )
204
255
  console.log(' Conflicting tasks:')
205
256
  for (const item of evaluatedCluster) {
257
+ const nextOccurrence = item.occurrences[0]?.toISOString() ?? 'n/a'
258
+ const preview =
259
+ item.occurrences
260
+ .slice(0, 3)
261
+ .map((occurrence) => occurrence.toISOString())
262
+ .join(', ') || 'n/a'
206
263
  const statusSuffix = '⚠️ violates window'
207
264
  console.log(
208
- ` • ${item.task.id} | start=${item.start?.toISOString() ?? 'n/a'} | ` +
265
+ ` • ${item.task.id} | next=${nextOccurrence} | upcoming=[${preview}] | ` +
209
266
  `createdAt=${item.task.createdAt.toISOString()} | updatedAt=${item.task.updatedAt.toISOString()} ${statusSuffix}`,
210
267
  )
211
268
  }
@@ -218,6 +275,12 @@ const main = async () => {
218
275
  dryRunLabel,
219
276
  })
220
277
 
278
+ if (cancelResult.status === 'error') {
279
+ throw new Error(
280
+ 'QStash cancellation failed; rerun after resolving token/availability issues.',
281
+ )
282
+ }
283
+
221
284
  await prisma.task.delete({ where: { id: candidate.task.id } })
222
285
  console.log(
223
286
  ` Deleted: ${candidate.task.id} (qstash: ${candidate.task.qstashMessageId ?? 'none'} | cancel=${cancelResult.status})`,
@@ -227,6 +290,7 @@ const main = async () => {
227
290
  ` Failed to delete ${candidate.task.id}:`,
228
291
  (error as Error).message,
229
292
  )
293
+ throw error
230
294
  }
231
295
  }
232
296
  } else {