@ossy/platform 1.22.4 → 1.22.6
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/package.json +5 -6
- package/src/index.js +2 -0
- package/src/tasks/cron.js +86 -0
- package/src/tasks/glob.js +12 -0
- package/src/tasks/task-loader.js +75 -0
- package/src/tasks/task-registry.js +17 -0
- package/src/tasks/task-service.js +124 -0
- package/src/worker-entry.js +0 -17
- package/src/worker-runtime.js +0 -127
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/platform",
|
|
3
|
-
"version": "1.22.
|
|
3
|
+
"version": "1.22.6",
|
|
4
4
|
"description": "Ossy application server runtime",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -14,8 +14,7 @@
|
|
|
14
14
|
"./proxy-internal": "./src/proxy-internal.js",
|
|
15
15
|
"./runtime": "./src/runtime.js",
|
|
16
16
|
"./site-loader": "./src/site-loader.js",
|
|
17
|
-
"./
|
|
18
|
-
"./worker-runtime": "./src/worker-runtime.js"
|
|
17
|
+
"./tasks": "./src/index.js"
|
|
19
18
|
},
|
|
20
19
|
"scripts": {
|
|
21
20
|
"start": "PORT=3003 node -e \"import('./src/server.js').then(m => m.startServer())\""
|
|
@@ -24,8 +23,8 @@
|
|
|
24
23
|
"author": "Ossy <yourfriends@ossy.se> (https://ossy.se)",
|
|
25
24
|
"license": "MIT",
|
|
26
25
|
"dependencies": {
|
|
27
|
-
"@ossy/router": "^1.23.
|
|
28
|
-
"@ossy/sdk": "^1.23.
|
|
26
|
+
"@ossy/router": "^1.23.6",
|
|
27
|
+
"@ossy/sdk": "^1.23.6",
|
|
29
28
|
"cookie-parser": "^1.4.7",
|
|
30
29
|
"dotenv": ">=16.0.0 <18.0.0",
|
|
31
30
|
"express": ">=5.0.0 <6.0.0",
|
|
@@ -35,5 +34,5 @@
|
|
|
35
34
|
"src",
|
|
36
35
|
"Dockerfile"
|
|
37
36
|
],
|
|
38
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "3454264bf96915a89b08c1855dd36bc54a8e6d26"
|
|
39
38
|
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal cron expression matcher for the 5-field standard format:
|
|
3
|
+
* minute hour day-of-month month day-of-week
|
|
4
|
+
*
|
|
5
|
+
* Each field supports:
|
|
6
|
+
* * — any value
|
|
7
|
+
* n — exact value
|
|
8
|
+
* a-b — inclusive range
|
|
9
|
+
* a,b,c — comma-separated list (each item may itself be a range or step)
|
|
10
|
+
* *\/n — step from 0 (e.g. *\/5 on minutes = 0,5,10,15,20,25,30,35,40,45,50,55)
|
|
11
|
+
* a-b\/n — step within range
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse a single cron field against a numeric value.
|
|
16
|
+
* @param {string} field - One cron field token.
|
|
17
|
+
* @param {number} value - Current time component to test.
|
|
18
|
+
* @param {number} min - Minimum valid value for this field.
|
|
19
|
+
* @param {number} max - Maximum valid value for this field.
|
|
20
|
+
* @returns {boolean}
|
|
21
|
+
*/
|
|
22
|
+
function matchField(field, value, min, max) {
|
|
23
|
+
if (field === '*') return true
|
|
24
|
+
|
|
25
|
+
for (const part of field.split(',')) {
|
|
26
|
+
if (matchPart(part.trim(), value, min, max)) return true
|
|
27
|
+
}
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function matchPart(part, value, min, max) {
|
|
32
|
+
// Step: */n or a-b/n
|
|
33
|
+
if (part.includes('/')) {
|
|
34
|
+
const [rangePart, stepStr] = part.split('/')
|
|
35
|
+
const step = parseInt(stepStr, 10)
|
|
36
|
+
if (isNaN(step) || step <= 0) return false
|
|
37
|
+
|
|
38
|
+
const [rangeMin, rangeMax] = rangePart === '*'
|
|
39
|
+
? [min, max]
|
|
40
|
+
: rangePart.includes('-')
|
|
41
|
+
? rangePart.split('-').map(Number)
|
|
42
|
+
: [parseInt(rangePart, 10), max]
|
|
43
|
+
|
|
44
|
+
if (value < rangeMin || value > rangeMax) return false
|
|
45
|
+
return (value - rangeMin) % step === 0
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Range: a-b
|
|
49
|
+
if (part.includes('-')) {
|
|
50
|
+
const [lo, hi] = part.split('-').map(Number)
|
|
51
|
+
return value >= lo && value <= hi
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Exact
|
|
55
|
+
return parseInt(part, 10) === value
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Returns true when the given cron expression matches the provided Date.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} expression - 5-field cron string (e.g. "* /5 * * * *" without the space).
|
|
62
|
+
* @param {Date} date - Date to test against (defaults to now).
|
|
63
|
+
* @returns {boolean}
|
|
64
|
+
*/
|
|
65
|
+
export function matchesCron(expression, date = new Date()) {
|
|
66
|
+
if (!expression || typeof expression !== 'string') return false
|
|
67
|
+
|
|
68
|
+
const fields = expression.trim().split(/\s+/)
|
|
69
|
+
if (fields.length !== 5) return false
|
|
70
|
+
|
|
71
|
+
const [minuteF, hourF, domF, monthF, dowF] = fields
|
|
72
|
+
|
|
73
|
+
const minute = date.getMinutes() // 0–59
|
|
74
|
+
const hour = date.getHours() // 0–23
|
|
75
|
+
const dom = date.getDate() // 1–31
|
|
76
|
+
const month = date.getMonth() + 1 // 1–12
|
|
77
|
+
const dow = date.getDay() // 0–6 (0=Sunday)
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
matchField(minuteF, minute, 0, 59) &&
|
|
81
|
+
matchField(hourF, hour, 0, 23) &&
|
|
82
|
+
matchField(domF, dom, 1, 31) &&
|
|
83
|
+
matchField(monthF, month, 1, 12) &&
|
|
84
|
+
matchField(dowF, dow, 0, 6)
|
|
85
|
+
)
|
|
86
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Glob matcher: ** matches any chars including /, * matches any chars except /.
|
|
3
|
+
*/
|
|
4
|
+
export function matchesGlob(pattern, value) {
|
|
5
|
+
if (!pattern || value == null) return false
|
|
6
|
+
const escaped = pattern
|
|
7
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
8
|
+
.replace(/\*\*/g, '§§') // protect ** before replacing *
|
|
9
|
+
.replace(/\*/g, '[^/]*')
|
|
10
|
+
.replace(/§§/g, '.*')
|
|
11
|
+
return new RegExp(`^${escaped}$`).test(value)
|
|
12
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises'
|
|
2
|
+
import { join, resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
const TASK_FILE_PATTERN = /\.task\.(mjs|cjs|js)$/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Discover all *.task.js files from:
|
|
8
|
+
* 1. The cwd's own src/ directory (recursive)
|
|
9
|
+
* 2. Any node_modules package that has "ossy": { "src": ... } in package.json
|
|
10
|
+
*
|
|
11
|
+
* Returns an array of file:// URLs suitable for dynamic import().
|
|
12
|
+
*/
|
|
13
|
+
export async function discoverTaskFiles({ cwd }) {
|
|
14
|
+
const files = []
|
|
15
|
+
|
|
16
|
+
// 1. Walk local src/
|
|
17
|
+
await walkDir(join(cwd, 'src'), files)
|
|
18
|
+
|
|
19
|
+
// 2. Walk node_modules packages with ossy.src
|
|
20
|
+
const nodeModulesDir = join(cwd, 'node_modules')
|
|
21
|
+
try {
|
|
22
|
+
const entries = await readdir(nodeModulesDir, { withFileTypes: true })
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue
|
|
25
|
+
|
|
26
|
+
// Handle scoped packages (@ossy/...)
|
|
27
|
+
if (entry.name.startsWith('@')) {
|
|
28
|
+
const scopedEntries = await readdir(join(nodeModulesDir, entry.name), { withFileTypes: true })
|
|
29
|
+
for (const scoped of scopedEntries) {
|
|
30
|
+
const pkgPath = join(nodeModulesDir, entry.name, scoped.name)
|
|
31
|
+
await checkPackageForTasks(pkgPath, files)
|
|
32
|
+
}
|
|
33
|
+
continue
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const pkgPath = join(nodeModulesDir, entry.name)
|
|
37
|
+
await checkPackageForTasks(pkgPath, files)
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
// node_modules doesn't exist yet
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return files
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function checkPackageForTasks(pkgPath, files) {
|
|
47
|
+
try {
|
|
48
|
+
const pkgJson = JSON.parse(await readFile(join(pkgPath, 'package.json'), 'utf8'))
|
|
49
|
+
const ossySrc = pkgJson?.ossy?.src
|
|
50
|
+
if (!ossySrc) return
|
|
51
|
+
await walkDir(join(pkgPath, ossySrc), files)
|
|
52
|
+
} catch {
|
|
53
|
+
// no package.json or not readable
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function walkDir(dir, files) {
|
|
58
|
+
try {
|
|
59
|
+
const entries = await readdir(dir, { withFileTypes: true })
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
const full = join(dir, entry.name)
|
|
62
|
+
if (entry.isDirectory()) {
|
|
63
|
+
await walkDir(full, files)
|
|
64
|
+
} else if (TASK_FILE_PATTERN.test(entry.name)) {
|
|
65
|
+
files.push(pathToFileUrl(full))
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
// dir doesn't exist
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function pathToFileUrl(filePath) {
|
|
74
|
+
return new URL('file://' + resolve(filePath)).href
|
|
75
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { discoverTaskFiles } from './task-loader.js'
|
|
2
|
+
import { TaskService } from './task-service.js'
|
|
3
|
+
|
|
4
|
+
export async function loadAndRegisterTasks({ cwd }) {
|
|
5
|
+
const taskFiles = await discoverTaskFiles({ cwd })
|
|
6
|
+
|
|
7
|
+
for (const fileUrl of taskFiles) {
|
|
8
|
+
try {
|
|
9
|
+
const taskModule = await import(fileUrl)
|
|
10
|
+
if (taskModule.metadata) {
|
|
11
|
+
TaskService.registerTask(taskModule)
|
|
12
|
+
}
|
|
13
|
+
} catch (err) {
|
|
14
|
+
console.error(`[TaskService] Failed to load task ${fileUrl}:`, err.message)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { matchesGlob } from './glob.js'
|
|
2
|
+
import { matchesCron } from './cron.js'
|
|
3
|
+
|
|
4
|
+
const SCHEDULER_INTERVAL_MS = 60_000
|
|
5
|
+
|
|
6
|
+
export class TaskService {
|
|
7
|
+
|
|
8
|
+
static _stopped = false
|
|
9
|
+
|
|
10
|
+
/** @type {Array<{ metadata: object, handler: function }>} */
|
|
11
|
+
static _tasks = []
|
|
12
|
+
|
|
13
|
+
/** @type {ReturnType<typeof setInterval> | null} */
|
|
14
|
+
static _schedulerInterval = null
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Register a task module to be dispatched on matching events
|
|
18
|
+
* and/or on a cron schedule.
|
|
19
|
+
* @param {{ metadata: { id: string, triggers?: Array, schedule?: string }, default: function }} taskModule
|
|
20
|
+
*/
|
|
21
|
+
static registerTask(taskModule) {
|
|
22
|
+
const handler = taskModule.default
|
|
23
|
+
const metadata = taskModule.metadata
|
|
24
|
+
|
|
25
|
+
if (typeof handler !== 'function') {
|
|
26
|
+
console.error(`[TaskService] Task "${metadata?.id}" has no default export function — skipping`)
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
if (!metadata?.id) {
|
|
30
|
+
console.error('[TaskService] Task has no metadata.id — skipping')
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
TaskService._tasks.push({ metadata, handler })
|
|
35
|
+
console.log(
|
|
36
|
+
`[TaskService] Registered task "${metadata.id}" with ${metadata.triggers?.length ?? 0} trigger(s)` +
|
|
37
|
+
(metadata.schedule ? ` and schedule "${metadata.schedule}"` : ''),
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Dispatches a single event (fullDocument from the changestream) to all
|
|
43
|
+
* registered tasks whose triggers match.
|
|
44
|
+
* @param {object} event - The eventstore fullDocument.
|
|
45
|
+
*/
|
|
46
|
+
static dispatch(event) {
|
|
47
|
+
if (!event) return
|
|
48
|
+
|
|
49
|
+
for (const { metadata, handler } of TaskService._tasks) {
|
|
50
|
+
const triggers = metadata.triggers ?? []
|
|
51
|
+
const matched = triggers.some(trigger => TaskService._matchesTrigger(trigger, event))
|
|
52
|
+
|
|
53
|
+
if (!matched) continue
|
|
54
|
+
|
|
55
|
+
console.log(`[TaskService] Dispatching task "${metadata.id}"`)
|
|
56
|
+
|
|
57
|
+
Promise.resolve()
|
|
58
|
+
.then(() => handler({ event, sdk: null }))
|
|
59
|
+
.catch(error =>
|
|
60
|
+
console.error(`[TaskService] Task "${metadata.id}" failed`, error),
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Starts a 60-second interval that fires each scheduled task whose cron
|
|
67
|
+
* expression matches the current minute. Calls the handler directly
|
|
68
|
+
* (no synthetic MongoDB events).
|
|
69
|
+
*/
|
|
70
|
+
static startScheduler() {
|
|
71
|
+
const scheduledTasks = TaskService._tasks.filter(t => !!t.metadata.schedule)
|
|
72
|
+
if (scheduledTasks.length === 0) return
|
|
73
|
+
|
|
74
|
+
console.log(`[TaskService] Scheduler started for ${scheduledTasks.length} task(s)`)
|
|
75
|
+
|
|
76
|
+
TaskService._stopped = false
|
|
77
|
+
TaskService._schedulerInterval = setInterval(() => {
|
|
78
|
+
if (TaskService._stopped) return
|
|
79
|
+
|
|
80
|
+
const now = new Date()
|
|
81
|
+
|
|
82
|
+
for (const { metadata, handler } of scheduledTasks) {
|
|
83
|
+
if (!matchesCron(metadata.schedule, now)) continue
|
|
84
|
+
|
|
85
|
+
console.log(`[TaskService] Schedule fired for task "${metadata.id}"`)
|
|
86
|
+
|
|
87
|
+
Promise.resolve()
|
|
88
|
+
.then(() => handler({ event: { type: 'scheduled', taskId: metadata.id }, sdk: null }))
|
|
89
|
+
.catch(error =>
|
|
90
|
+
console.error(`[TaskService] Scheduled task "${metadata.id}" failed`, error),
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
}, SCHEDULER_INTERVAL_MS)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Clears the scheduler interval.
|
|
98
|
+
*/
|
|
99
|
+
static stop() {
|
|
100
|
+
TaskService._stopped = true
|
|
101
|
+
if (TaskService._schedulerInterval !== null) {
|
|
102
|
+
clearInterval(TaskService._schedulerInterval)
|
|
103
|
+
TaskService._schedulerInterval = null
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Returns true when a trigger descriptor matches the given eventstore document.
|
|
109
|
+
*
|
|
110
|
+
* Matching rules:
|
|
111
|
+
* trigger.aggregateType — exact match against event.aggregateType
|
|
112
|
+
* trigger.event — exact match against event.type
|
|
113
|
+
* trigger.resource.type — glob match (supports *) against event.payload.type
|
|
114
|
+
* trigger.location.startsWith — prefix match against event.payload.location
|
|
115
|
+
*/
|
|
116
|
+
static _matchesTrigger(trigger, event) {
|
|
117
|
+
if (trigger.aggregateType && trigger.aggregateType !== event.aggregateType) return false
|
|
118
|
+
if (trigger.event && trigger.event !== event.type) return false
|
|
119
|
+
if (trigger.resource?.type && !matchesGlob(trigger.resource.type, event.payload?.type)) return false
|
|
120
|
+
if (trigger.location?.startsWith && !event.payload?.location?.startsWith?.(trigger.location.startsWith)) return false
|
|
121
|
+
return true
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
}
|
package/src/worker-entry.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import 'dotenv/config'
|
|
2
|
-
import fs from 'node:fs'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import { fileURLToPath } from 'node:url'
|
|
5
|
-
import { runWorkerScheduler } from './worker-runtime.js'
|
|
6
|
-
|
|
7
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
8
|
-
const buildDir = process.env.OSSY_BUILD_DIR
|
|
9
|
-
? path.resolve(process.env.OSSY_BUILD_DIR)
|
|
10
|
-
: path.resolve(__dirname, '..', '..', '..', 'build')
|
|
11
|
-
const manifestPath = path.join(buildDir, 'manifest.json')
|
|
12
|
-
|
|
13
|
-
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
|
|
14
|
-
const entries = Array.isArray(manifest.entries) ? manifest.entries : []
|
|
15
|
-
const tasks = entries.filter((e) => e.type === 'task')
|
|
16
|
-
|
|
17
|
-
runWorkerScheduler(tasks, buildDir)
|
package/src/worker-runtime.js
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import path from 'node:path'
|
|
2
|
-
import { pathToFileURL } from 'node:url'
|
|
3
|
-
import { SDK } from '@ossy/sdk'
|
|
4
|
-
|
|
5
|
-
function resolveTaskEntryUrl (entryUrl, buildDir) {
|
|
6
|
-
if (!entryUrl) throw new Error('[@ossy/platform][worker] Task entry URL missing')
|
|
7
|
-
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(entryUrl)) return entryUrl
|
|
8
|
-
const trimmed = entryUrl.startsWith('/') ? entryUrl.slice(1) : entryUrl
|
|
9
|
-
return pathToFileURL(path.join(buildDir, 'public', trimmed)).href
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* @param {Array<{ id: string, entry: string }>} tasks
|
|
14
|
-
* @param {string} buildDir - absolute path to the build directory
|
|
15
|
-
*/
|
|
16
|
-
export function runWorkerScheduler (tasks, buildDir) {
|
|
17
|
-
const sdk = SDK.of({
|
|
18
|
-
workspaceId: process.env.OSSY_WORKSPACE_ID,
|
|
19
|
-
apiUrl: process.env.OSSY_API_URL,
|
|
20
|
-
authorization: process.env.OSSY_API_TOKEN,
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
const jobsClient = /** @type {{ getUnprocessed: () => Promise<unknown[]> }} */ (sdk.jobs)
|
|
24
|
-
let status = 'running'
|
|
25
|
-
|
|
26
|
-
console.log('Starting scheduler')
|
|
27
|
-
main()
|
|
28
|
-
|
|
29
|
-
setInterval(() => {
|
|
30
|
-
if (status === 'running') return
|
|
31
|
-
status = 'running'
|
|
32
|
-
try {
|
|
33
|
-
main()
|
|
34
|
-
} catch (error) {
|
|
35
|
-
console.log('Error running main')
|
|
36
|
-
console.error(error)
|
|
37
|
-
status = 'idle'
|
|
38
|
-
}
|
|
39
|
-
}, 3000)
|
|
40
|
-
|
|
41
|
-
function main () {
|
|
42
|
-
console.log('Looking for jobs')
|
|
43
|
-
jobsClient
|
|
44
|
-
.getUnprocessed()
|
|
45
|
-
.then(async (jobs) => {
|
|
46
|
-
if (!jobs || !jobs.length) {
|
|
47
|
-
console.log('No jobs found, going idle')
|
|
48
|
-
status = 'idle'
|
|
49
|
-
return
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const jobsGroupedByResourceId = groupJobsByResourceId(jobs)
|
|
53
|
-
console.log(`Found ${jobs.length} jobs between ${jobsGroupedByResourceId.length} resources`)
|
|
54
|
-
|
|
55
|
-
const processedGroups = jobsGroupedByResourceId.map(([resourceId, groupJobs]) => {
|
|
56
|
-
console.log(`Processing group for resourceId ${resourceId}`)
|
|
57
|
-
return processJobsSequentially(groupJobs)
|
|
58
|
-
.then(() => console.log(`Completed group for resourceId ${resourceId}`))
|
|
59
|
-
.catch((err) => {
|
|
60
|
-
console.log(`Failed to process group for resourceId ${resourceId}`)
|
|
61
|
-
console.error(err)
|
|
62
|
-
})
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
await Promise.allSettled(processedGroups)
|
|
67
|
-
console.log('Finished processing of groups...')
|
|
68
|
-
console.log('Going idle')
|
|
69
|
-
status = 'idle'
|
|
70
|
-
console.log('----------------------------------')
|
|
71
|
-
console.groupEnd()
|
|
72
|
-
} catch (error) {
|
|
73
|
-
console.log('Error processing groups')
|
|
74
|
-
console.error(error)
|
|
75
|
-
status = 'idle'
|
|
76
|
-
}
|
|
77
|
-
})
|
|
78
|
-
.catch((error) => {
|
|
79
|
-
console.log('Error getting jobs')
|
|
80
|
-
console.error(error)
|
|
81
|
-
status = 'idle'
|
|
82
|
-
})
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function groupJobsByResourceId (jobList) {
|
|
86
|
-
return Object.entries(
|
|
87
|
-
jobList.reduce((acc, job) => {
|
|
88
|
-
const content = /** @type {{ resourceId?: string }} */ (job.content || {})
|
|
89
|
-
const rid = content.resourceId
|
|
90
|
-
return {
|
|
91
|
-
...acc,
|
|
92
|
-
[rid]: [...(acc[rid] || []), job],
|
|
93
|
-
}
|
|
94
|
-
}, /** @type {Record<string, unknown[]>} */ ({}))
|
|
95
|
-
)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async function processJobsSequentially (jobList) {
|
|
99
|
-
console.log(`Processing ${jobList.length} jobs`)
|
|
100
|
-
for (const job of jobList) {
|
|
101
|
-
console.log(`Processing job ${job.id}`)
|
|
102
|
-
const task = tasks.find((t) => t.id === job.type)
|
|
103
|
-
if (!task) {
|
|
104
|
-
console.log('No handler found for job', job.id)
|
|
105
|
-
continue
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
console.log(`Handler found for ${task.id}`)
|
|
109
|
-
try {
|
|
110
|
-
const mod = await import(resolveTaskEntryUrl(task.entry, buildDir))
|
|
111
|
-
const jobSdk = SDK.of({
|
|
112
|
-
workspaceId: job.belongsTo,
|
|
113
|
-
authorization: process.env.OSSY_API_TOKEN,
|
|
114
|
-
})
|
|
115
|
-
const runner = typeof mod.run === 'function' ? mod.run : mod.default
|
|
116
|
-
if (typeof runner !== 'function') {
|
|
117
|
-
console.log(`Task ${task.id} has no callable handler`)
|
|
118
|
-
continue
|
|
119
|
-
}
|
|
120
|
-
await runner({ sdk: jobSdk, job }).catch(() => {})
|
|
121
|
-
} catch (error) {
|
|
122
|
-
console.error(error)
|
|
123
|
-
console.log('Failed to processing job')
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|