@superinterface/server 1.0.36 → 1.0.37
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/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +2 -2
- package/.next/cache/.tsbuildinfo +1 -1
- package/.next/cache/eslint/.cache_btwyo7 +1 -1
- package/.next/fallback-build-manifest.json +2 -2
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +1 -1
- package/.next/server/app/api/assistants/[assistantId]/functions/[functionId]/route.js +2 -2
- package/.next/server/app/api/assistants/[assistantId]/functions/[functionId]/route.js.nft.json +1 -1
- package/.next/server/app/api/assistants/[assistantId]/functions/route.js +2 -2
- package/.next/server/app/api/assistants/[assistantId]/functions/route.js.nft.json +1 -1
- package/.next/server/app/api/messages/route.js +3 -3
- package/.next/server/app/api/messages/route.js.nft.json +1 -1
- package/.next/server/app/api/tasks/[taskId]/route.js +2 -2
- package/.next/server/app/api/tasks/[taskId]/route.js.nft.json +1 -1
- package/.next/server/app/api/tasks/callback/route.js +5 -5
- package/.next/server/app/api/tasks/callback/route.js.nft.json +1 -1
- package/.next/server/app/api/tasks/route.js +2 -2
- package/.next/server/app/api/tasks/route.js.nft.json +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +1 -1
- package/.next/server/chunks/[root-of-the-server]__0a426407._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0a426407._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__1a7a04d0._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__1a7a04d0._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__29b43490._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__29b43490._.js.map +1 -1
- package/.next/server/chunks/{[root-of-the-server]__ed6cf593._.js → [root-of-the-server]__3a25f63d._.js} +4 -4
- package/.next/server/chunks/[root-of-the-server]__3a25f63d._.js.map +1 -0
- package/.next/server/chunks/{[root-of-the-server]__13c6bd62._.js → [root-of-the-server]__4d829477._.js} +4 -4
- package/.next/server/chunks/[root-of-the-server]__4d829477._.js.map +1 -0
- package/.next/server/chunks/{[root-of-the-server]__b9a334c3._.js → [root-of-the-server]__6169c901._.js} +4 -4
- package/.next/server/chunks/[root-of-the-server]__6169c901._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__630959e5._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__630959e5._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__6317294d._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__6317294d._.js.map +1 -0
- package/.next/server/chunks/{[root-of-the-server]__72b72b9e._.js → [root-of-the-server]__a6d26a37._.js} +2 -2
- package/.next/server/chunks/[root-of-the-server]__a6d26a37._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__a9fab3b2._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__a9fab3b2._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__dd176cb5._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__dd176cb5._.js.map +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/trace +1 -1
- package/dist/app/api/tasks/[taskId]/buildRoute.d.ts.map +1 -1
- package/dist/app/api/tasks/[taskId]/buildRoute.js +21 -0
- package/dist/app/api/tasks/buildRoute.d.ts.map +1 -1
- package/dist/app/api/tasks/buildRoute.js +18 -1
- package/dist/lib/errors/index.d.ts +4 -0
- package/dist/lib/errors/index.d.ts.map +1 -1
- package/dist/lib/errors/index.js +7 -1
- package/dist/lib/functions/handleFunction/tasks/handleCreateTask.d.ts.map +1 -1
- package/dist/lib/functions/handleFunction/tasks/handleCreateTask.js +27 -4
- package/dist/lib/functions/handleFunction/tasks/handleUpdateTask.d.ts.map +1 -1
- package/dist/lib/functions/handleFunction/tasks/handleUpdateTask.js +51 -7
- package/dist/lib/tasks/ensureTaskSchedule.d.ts +13 -0
- package/dist/lib/tasks/ensureTaskSchedule.d.ts.map +1 -0
- package/dist/lib/tasks/ensureTaskSchedule.js +15 -0
- package/dist/lib/tasks/getTaskScheduleConflict.d.ts +22 -0
- package/dist/lib/tasks/getTaskScheduleConflict.d.ts.map +1 -0
- package/dist/lib/tasks/getTaskScheduleConflict.js +49 -0
- package/package.json +5 -2
- package/scripts/cleanupTaskConflicts.ts +218 -0
- package/scripts/runTests.ts +26 -0
- package/.next/server/chunks/[root-of-the-server]__13c6bd62._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__25ee13bc._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__25ee13bc._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__29635e8e._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__29635e8e._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__464a4377._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__464a4377._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__5d09614a._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__5d09614a._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__72b72b9e._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__b9a334c3._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__ed6cf593._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__f845ef25._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__f845ef25._.js.map +0 -1
- /package/.next/static/{umwU4D-6dg7tl5DCOW1vq → a3KG4O2A5sJO2AvvW4J1L}/_buildManifest.js +0 -0
- /package/.next/static/{umwU4D-6dg7tl5DCOW1vq → a3KG4O2A5sJO2AvvW4J1L}/_clientMiddlewareManifest.json +0 -0
- /package/.next/static/{umwU4D-6dg7tl5DCOW1vq → a3KG4O2A5sJO2AvvW4J1L}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import dayjs from 'dayjs'
|
|
2
|
+
import utc from 'dayjs/plugin/utc'
|
|
3
|
+
import { PrismaClient } from '@prisma/client'
|
|
4
|
+
import { cancelScheduledTask } from '@/lib/tasks/cancelScheduledTask'
|
|
5
|
+
import { FIFTEEN_MINUTES_IN_MS } from '@/lib/tasks/getTaskScheduleConflict'
|
|
6
|
+
import { TaskScheduleConflictError } from '@/lib/errors'
|
|
7
|
+
|
|
8
|
+
dayjs.extend(utc)
|
|
9
|
+
|
|
10
|
+
type CliOptions = {
|
|
11
|
+
dryRun: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const parseCliOptions = (): CliOptions => {
|
|
15
|
+
const argv = process.argv.slice(2)
|
|
16
|
+
const dryRun = !argv.some((arg) =>
|
|
17
|
+
['--force', '--no-dry-run', '--apply'].includes(arg),
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
return { dryRun }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type TaskWithDates = Awaited<
|
|
24
|
+
ReturnType<PrismaClient['task']['findMany']>
|
|
25
|
+
>[number]
|
|
26
|
+
|
|
27
|
+
const getScheduleStart = (task: TaskWithDates) => {
|
|
28
|
+
const start = (task.schedule as { start?: unknown } | null)?.start
|
|
29
|
+
if (typeof start !== 'string') return null
|
|
30
|
+
const parsed = dayjs(start).utc()
|
|
31
|
+
return parsed.isValid() ? parsed : null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const getLastTouchedAt = (task: TaskWithDates) => {
|
|
35
|
+
const updated = dayjs(task.updatedAt).utc()
|
|
36
|
+
const created = dayjs(task.createdAt).utc()
|
|
37
|
+
return updated.isAfter(created) ? updated : created
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const main = async () => {
|
|
41
|
+
const prisma = new PrismaClient()
|
|
42
|
+
const options = parseCliOptions()
|
|
43
|
+
const dryRunLabel = options.dryRun ? '[DRY RUN]' : '[APPLY]'
|
|
44
|
+
|
|
45
|
+
console.log(
|
|
46
|
+
`${dryRunLabel} Starting duplicate task audit – ` +
|
|
47
|
+
`${TaskScheduleConflictError.defaultMessage}`,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const tasks = await prisma.task.findMany({
|
|
52
|
+
orderBy: { createdAt: 'asc' },
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const groups = new Map<string, TaskWithDates[]>()
|
|
56
|
+
for (const task of tasks) {
|
|
57
|
+
const key = `${task.threadId}::${task.key}`
|
|
58
|
+
const list = groups.get(key) ?? []
|
|
59
|
+
list.push(task)
|
|
60
|
+
groups.set(key, list)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let conflictGroupCount = 0
|
|
64
|
+
let candidateDeleteCount = 0
|
|
65
|
+
|
|
66
|
+
for (const [groupKey, taskGroup] of groups.entries()) {
|
|
67
|
+
if (taskGroup.length < 2) continue
|
|
68
|
+
|
|
69
|
+
const sortedByStart = taskGroup
|
|
70
|
+
.map((task) => ({
|
|
71
|
+
task,
|
|
72
|
+
start: getScheduleStart(task),
|
|
73
|
+
}))
|
|
74
|
+
.sort((a, b) => {
|
|
75
|
+
if (!a.start && !b.start) return 0
|
|
76
|
+
if (!a.start) return 1
|
|
77
|
+
if (!b.start) return -1
|
|
78
|
+
return a.start.valueOf() - b.start.valueOf()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const validEntries = sortedByStart
|
|
82
|
+
.map((entry, index) => ({ ...entry, index }))
|
|
83
|
+
.filter((entry): entry is typeof entry & { start: dayjs.Dayjs } => {
|
|
84
|
+
if (!entry.start) {
|
|
85
|
+
console.log(
|
|
86
|
+
`${dryRunLabel} Skipping task without valid start: ${entry.task.id}`,
|
|
87
|
+
)
|
|
88
|
+
return false
|
|
89
|
+
}
|
|
90
|
+
return true
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
if (validEntries.length < 2) continue
|
|
94
|
+
|
|
95
|
+
const parent = Array.from({ length: validEntries.length }, (_, i) => i)
|
|
96
|
+
const find = (i: number): number => {
|
|
97
|
+
if (parent[i] === i) return i
|
|
98
|
+
parent[i] = find(parent[i])
|
|
99
|
+
return parent[i]
|
|
100
|
+
}
|
|
101
|
+
const union = (a: number, b: number) => {
|
|
102
|
+
const rootA = find(a)
|
|
103
|
+
const rootB = find(b)
|
|
104
|
+
if (rootA === rootB) return
|
|
105
|
+
parent[rootB] = rootA
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (let i = 0; i < validEntries.length; i += 1) {
|
|
109
|
+
const current = validEntries[i]
|
|
110
|
+
for (let j = i + 1; j < validEntries.length; j += 1) {
|
|
111
|
+
const compare = validEntries[j]
|
|
112
|
+
const diffMs = compare.start.diff(current.start)
|
|
113
|
+
if (diffMs >= FIFTEEN_MINUTES_IN_MS) break
|
|
114
|
+
if (Math.abs(diffMs) < FIFTEEN_MINUTES_IN_MS) {
|
|
115
|
+
union(i, j)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const clusters = new Map<number, TaskWithDates[]>()
|
|
121
|
+
for (let i = 0; i < validEntries.length; i += 1) {
|
|
122
|
+
const root = find(i)
|
|
123
|
+
const cluster = clusters.get(root) ?? []
|
|
124
|
+
cluster.push(validEntries[i].task)
|
|
125
|
+
clusters.set(root, cluster)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const overlapping = Array.from(clusters.values()).filter(
|
|
129
|
+
(cluster) => cluster.length > 1,
|
|
130
|
+
)
|
|
131
|
+
if (!overlapping.length) continue
|
|
132
|
+
|
|
133
|
+
conflictGroupCount += overlapping.length
|
|
134
|
+
|
|
135
|
+
console.log(`\n${dryRunLabel} Conflict group: ${groupKey}`)
|
|
136
|
+
|
|
137
|
+
for (const cluster of overlapping) {
|
|
138
|
+
const evaluatedCluster = cluster.map((task) => ({
|
|
139
|
+
task,
|
|
140
|
+
start: getScheduleStart(task),
|
|
141
|
+
lastTouchedAt: getLastTouchedAt(task),
|
|
142
|
+
}))
|
|
143
|
+
|
|
144
|
+
const keepCandidate = evaluatedCluster.reduce((latest, current) => {
|
|
145
|
+
if (!latest) return current
|
|
146
|
+
if (current.lastTouchedAt.isAfter(latest.lastTouchedAt)) {
|
|
147
|
+
return current
|
|
148
|
+
}
|
|
149
|
+
return latest
|
|
150
|
+
}, evaluatedCluster[0])
|
|
151
|
+
|
|
152
|
+
const toDelete = evaluatedCluster.filter(
|
|
153
|
+
(item) => item.task.id !== keepCandidate?.task.id,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
candidateDeleteCount += toDelete.length
|
|
157
|
+
|
|
158
|
+
console.log(
|
|
159
|
+
` Keep: ${keepCandidate?.task.id} (lastTouched: ${keepCandidate?.lastTouchedAt.toISOString()})`,
|
|
160
|
+
)
|
|
161
|
+
console.log(' Conflicting tasks:')
|
|
162
|
+
for (const item of evaluatedCluster) {
|
|
163
|
+
const statusSuffix = '⚠️ violates window'
|
|
164
|
+
console.log(
|
|
165
|
+
` • ${item.task.id} | start=${item.start?.toISOString() ?? 'n/a'} | ` +
|
|
166
|
+
`createdAt=${item.task.createdAt.toISOString()} | updatedAt=${item.task.updatedAt.toISOString()} ${statusSuffix}`,
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!options.dryRun) {
|
|
171
|
+
for (const candidate of toDelete) {
|
|
172
|
+
try {
|
|
173
|
+
if (candidate.task.qstashMessageId) {
|
|
174
|
+
await cancelScheduledTask({ task: candidate.task })
|
|
175
|
+
}
|
|
176
|
+
await prisma.task.delete({ where: { id: candidate.task.id } })
|
|
177
|
+
console.log(
|
|
178
|
+
` Deleted: ${candidate.task.id} (qstash: ${candidate.task.qstashMessageId ?? 'none'})`,
|
|
179
|
+
)
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error(
|
|
182
|
+
` Failed to delete ${candidate.task.id}:`,
|
|
183
|
+
(error as Error).message,
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
for (const candidate of toDelete) {
|
|
189
|
+
console.log(
|
|
190
|
+
` Would delete: ${candidate.task.id} (qstash: ${candidate.task.qstashMessageId ?? 'none'})`,
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (conflictGroupCount === 0) {
|
|
198
|
+
console.log(`${dryRunLabel} No conflicting task groups detected.`)
|
|
199
|
+
} else {
|
|
200
|
+
console.log(
|
|
201
|
+
`\n${dryRunLabel} Summary: ${conflictGroupCount} conflicting group(s); ` +
|
|
202
|
+
`${candidateDeleteCount} task(s) marked for deletion.`,
|
|
203
|
+
)
|
|
204
|
+
if (options.dryRun) {
|
|
205
|
+
console.log(
|
|
206
|
+
`${dryRunLabel} Re-run with '--force' to delete and cancel scheduled jobs.`,
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} finally {
|
|
211
|
+
await prisma.$disconnect()
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
main().catch((error) => {
|
|
216
|
+
console.error('Unhandled error while auditing tasks:', error)
|
|
217
|
+
process.exitCode = 1
|
|
218
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
import { randomUUID } from 'node:crypto'
|
|
3
|
+
|
|
4
|
+
const dbName = `superinterface_test_${randomUUID()}`
|
|
5
|
+
const dbUrl = `postgresql://postgres:postgres@localhost:5432/${dbName}`
|
|
6
|
+
|
|
7
|
+
console.log(`Creating database ${dbName}`)
|
|
8
|
+
execSync(`createdb -h localhost -U postgres ${dbName}`, {
|
|
9
|
+
stdio: 'inherit',
|
|
10
|
+
env: { ...process.env, PGPASSWORD: 'postgres' },
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const commonEnv = { ...process.env, DATABASE_URL: dbUrl, DIRECT_URL: dbUrl }
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
execSync('npx prisma migrate deploy', { stdio: 'inherit', env: commonEnv })
|
|
17
|
+
execSync(
|
|
18
|
+
'node --import tsx --experimental-test-module-mocks --test tests/**/*.test.ts',
|
|
19
|
+
{
|
|
20
|
+
stdio: 'inherit',
|
|
21
|
+
env: { ...commonEnv, NODE_ENV: 'test' },
|
|
22
|
+
},
|
|
23
|
+
)
|
|
24
|
+
} finally {
|
|
25
|
+
console.log(`Database ${dbName} preserved for inspection`)
|
|
26
|
+
}
|