@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.
Files changed (83) 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/api/assistants/[assistantId]/functions/[functionId]/route.js +2 -2
  9. package/.next/server/app/api/assistants/[assistantId]/functions/[functionId]/route.js.nft.json +1 -1
  10. package/.next/server/app/api/assistants/[assistantId]/functions/route.js +2 -2
  11. package/.next/server/app/api/assistants/[assistantId]/functions/route.js.nft.json +1 -1
  12. package/.next/server/app/api/messages/route.js +3 -3
  13. package/.next/server/app/api/messages/route.js.nft.json +1 -1
  14. package/.next/server/app/api/tasks/[taskId]/route.js +2 -2
  15. package/.next/server/app/api/tasks/[taskId]/route.js.nft.json +1 -1
  16. package/.next/server/app/api/tasks/callback/route.js +5 -5
  17. package/.next/server/app/api/tasks/callback/route.js.nft.json +1 -1
  18. package/.next/server/app/api/tasks/route.js +2 -2
  19. package/.next/server/app/api/tasks/route.js.nft.json +1 -1
  20. package/.next/server/app/index.html +1 -1
  21. package/.next/server/app/index.rsc +1 -1
  22. package/.next/server/chunks/[root-of-the-server]__0a426407._.js +3 -0
  23. package/.next/server/chunks/[root-of-the-server]__0a426407._.js.map +1 -0
  24. package/.next/server/chunks/[root-of-the-server]__1a7a04d0._.js +3 -0
  25. package/.next/server/chunks/[root-of-the-server]__1a7a04d0._.js.map +1 -0
  26. package/.next/server/chunks/[root-of-the-server]__29b43490._.js +3 -3
  27. package/.next/server/chunks/[root-of-the-server]__29b43490._.js.map +1 -1
  28. package/.next/server/chunks/{[root-of-the-server]__ed6cf593._.js → [root-of-the-server]__3a25f63d._.js} +4 -4
  29. package/.next/server/chunks/[root-of-the-server]__3a25f63d._.js.map +1 -0
  30. package/.next/server/chunks/{[root-of-the-server]__13c6bd62._.js → [root-of-the-server]__4d829477._.js} +4 -4
  31. package/.next/server/chunks/[root-of-the-server]__4d829477._.js.map +1 -0
  32. package/.next/server/chunks/{[root-of-the-server]__b9a334c3._.js → [root-of-the-server]__6169c901._.js} +4 -4
  33. package/.next/server/chunks/[root-of-the-server]__6169c901._.js.map +1 -0
  34. package/.next/server/chunks/[root-of-the-server]__630959e5._.js +3 -0
  35. package/.next/server/chunks/[root-of-the-server]__630959e5._.js.map +1 -0
  36. package/.next/server/chunks/[root-of-the-server]__6317294d._.js +3 -0
  37. package/.next/server/chunks/[root-of-the-server]__6317294d._.js.map +1 -0
  38. package/.next/server/chunks/{[root-of-the-server]__72b72b9e._.js → [root-of-the-server]__a6d26a37._.js} +2 -2
  39. package/.next/server/chunks/[root-of-the-server]__a6d26a37._.js.map +1 -0
  40. package/.next/server/chunks/[root-of-the-server]__a9fab3b2._.js +3 -0
  41. package/.next/server/chunks/[root-of-the-server]__a9fab3b2._.js.map +1 -0
  42. package/.next/server/chunks/[root-of-the-server]__dd176cb5._.js +3 -3
  43. package/.next/server/chunks/[root-of-the-server]__dd176cb5._.js.map +1 -1
  44. package/.next/server/pages/404.html +1 -1
  45. package/.next/server/pages/500.html +1 -1
  46. package/.next/trace +1 -1
  47. package/dist/app/api/tasks/[taskId]/buildRoute.d.ts.map +1 -1
  48. package/dist/app/api/tasks/[taskId]/buildRoute.js +21 -0
  49. package/dist/app/api/tasks/buildRoute.d.ts.map +1 -1
  50. package/dist/app/api/tasks/buildRoute.js +18 -1
  51. package/dist/lib/errors/index.d.ts +4 -0
  52. package/dist/lib/errors/index.d.ts.map +1 -1
  53. package/dist/lib/errors/index.js +7 -1
  54. package/dist/lib/functions/handleFunction/tasks/handleCreateTask.d.ts.map +1 -1
  55. package/dist/lib/functions/handleFunction/tasks/handleCreateTask.js +27 -4
  56. package/dist/lib/functions/handleFunction/tasks/handleUpdateTask.d.ts.map +1 -1
  57. package/dist/lib/functions/handleFunction/tasks/handleUpdateTask.js +51 -7
  58. package/dist/lib/tasks/ensureTaskSchedule.d.ts +13 -0
  59. package/dist/lib/tasks/ensureTaskSchedule.d.ts.map +1 -0
  60. package/dist/lib/tasks/ensureTaskSchedule.js +15 -0
  61. package/dist/lib/tasks/getTaskScheduleConflict.d.ts +22 -0
  62. package/dist/lib/tasks/getTaskScheduleConflict.d.ts.map +1 -0
  63. package/dist/lib/tasks/getTaskScheduleConflict.js +49 -0
  64. package/package.json +5 -2
  65. package/scripts/cleanupTaskConflicts.ts +218 -0
  66. package/scripts/runTests.ts +26 -0
  67. package/.next/server/chunks/[root-of-the-server]__13c6bd62._.js.map +0 -1
  68. package/.next/server/chunks/[root-of-the-server]__25ee13bc._.js +0 -3
  69. package/.next/server/chunks/[root-of-the-server]__25ee13bc._.js.map +0 -1
  70. package/.next/server/chunks/[root-of-the-server]__29635e8e._.js +0 -3
  71. package/.next/server/chunks/[root-of-the-server]__29635e8e._.js.map +0 -1
  72. package/.next/server/chunks/[root-of-the-server]__464a4377._.js +0 -3
  73. package/.next/server/chunks/[root-of-the-server]__464a4377._.js.map +0 -1
  74. package/.next/server/chunks/[root-of-the-server]__5d09614a._.js +0 -3
  75. package/.next/server/chunks/[root-of-the-server]__5d09614a._.js.map +0 -1
  76. package/.next/server/chunks/[root-of-the-server]__72b72b9e._.js.map +0 -1
  77. package/.next/server/chunks/[root-of-the-server]__b9a334c3._.js.map +0 -1
  78. package/.next/server/chunks/[root-of-the-server]__ed6cf593._.js.map +0 -1
  79. package/.next/server/chunks/[root-of-the-server]__f845ef25._.js +0 -3
  80. package/.next/server/chunks/[root-of-the-server]__f845ef25._.js.map +0 -1
  81. /package/.next/static/{umwU4D-6dg7tl5DCOW1vq → a3KG4O2A5sJO2AvvW4J1L}/_buildManifest.js +0 -0
  82. /package/.next/static/{umwU4D-6dg7tl5DCOW1vq → a3KG4O2A5sJO2AvvW4J1L}/_clientMiddlewareManifest.json +0 -0
  83. /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
+ }