velocious-jobler 0.0.2
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/LICENSE +21 -0
- package/README.md +99 -0
- package/build/src/backend/jobler-job-runner.d.ts +74 -0
- package/build/src/backend/jobler-job-runner.d.ts.map +1 -0
- package/build/src/backend/jobler-job-runner.js +87 -0
- package/build/src/database/migrations/20260630140000-create-jobler-jobs.d.ts +11 -0
- package/build/src/database/migrations/20260630140000-create-jobler-jobs.d.ts.map +1 -0
- package/build/src/database/migrations/20260630140000-create-jobler-jobs.js +33 -0
- package/build/src/frontend/job-progress-indicator.d.ts +56 -0
- package/build/src/frontend/job-progress-indicator.d.ts.map +1 -0
- package/build/src/frontend/job-progress-indicator.js +154 -0
- package/build/src/model-bases/jobler-job.d.ts +253 -0
- package/build/src/model-bases/jobler-job.d.ts.map +1 -0
- package/build/src/model-bases/jobler-job.js +197 -0
- package/build/src/models/jobler-job.d.ts +16 -0
- package/build/src/models/jobler-job.d.ts.map +1 -0
- package/build/src/models/jobler-job.js +31 -0
- package/build/src/resources/jobler-job-resource.d.ts +21 -0
- package/build/src/resources/jobler-job-resource.d.ts.map +1 -0
- package/build/src/resources/jobler-job-resource.js +27 -0
- package/build/velocious-package.d.ts +4 -0
- package/build/velocious-package.d.ts.map +1 -0
- package/build/velocious-package.js +3 -0
- package/package.json +77 -0
- package/src/backend/jobler-job-runner.js +100 -0
- package/src/database/migrations/20260630140000-create-jobler-jobs.js +36 -0
- package/src/frontend/job-progress-indicator.jsx +206 -0
- package/src/model-bases/jobler-job.js +238 -0
- package/src/models/jobler-job.js +36 -0
- package/src/resources/jobler-job-resource.js +33 -0
- package/velocious-package.js +5 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
export const JOBLER_JOB_STATUS_PENDING = "pending"
|
|
4
|
+
export const JOBLER_JOB_STATUS_RUNNING = "running"
|
|
5
|
+
export const JOBLER_JOB_STATUS_SUCCEEDED = "succeeded"
|
|
6
|
+
export const JOBLER_JOB_STATUS_FAILED = "failed"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The subset of a jobler-job model this runner needs. Any Velocious model with a
|
|
10
|
+
* `status` / `progress_current` / `progress_total` / `status_message` /
|
|
11
|
+
* `error_message` / `started_at` / `finished_at` schema satisfies it.
|
|
12
|
+
* @typedef {object} JoblerJobLike
|
|
13
|
+
* @property {(status: string) => void} setStatus
|
|
14
|
+
* @property {(current: number) => void} setProgressCurrent
|
|
15
|
+
* @property {(total: number | null) => void} setProgressTotal
|
|
16
|
+
* @property {(message: string | null) => void} setStatusMessage
|
|
17
|
+
* @property {(message: string | null) => void} setErrorMessage
|
|
18
|
+
* @property {(at: Date) => void} setStartedAt
|
|
19
|
+
* @property {(at: Date) => void} setFinishedAt
|
|
20
|
+
* @property {() => Promise<unknown>} save
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {object} JoblerProgressReporter
|
|
25
|
+
* @property {(args: {current?: number, total?: number | null, message?: string | null}) => Promise<void>} progress
|
|
26
|
+
* @property {(message: string) => Promise<void>} message
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Runs a unit of work while keeping a jobler-job's status and progress live. Each
|
|
31
|
+
* update is a `save()`, which — because the job is a broadcasting Velocious
|
|
32
|
+
* frontend-model resource — pushes a websocket update the UI can render as it
|
|
33
|
+
* happens. Framework-agnostic: pass any model that implements {@link JoblerJobLike}
|
|
34
|
+
* and, optionally, an `onError` reporter (e.g. your app's bug reporter).
|
|
35
|
+
*/
|
|
36
|
+
export default class JoblerJobRunner {
|
|
37
|
+
/**
|
|
38
|
+
* @param {{onError?: (error: Error, joblerJob: JoblerJobLike) => (void | Promise<void>)}} [args={}]
|
|
39
|
+
*/
|
|
40
|
+
constructor(args = {}) {
|
|
41
|
+
this.onError = args.onError
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Marks the job running, runs `work` (handed a progress reporter), then marks it
|
|
46
|
+
* succeeded. On failure it marks the job failed, calls `onError` if provided, and
|
|
47
|
+
* rethrows so the dispatching background job's retry policy still applies.
|
|
48
|
+
* @param {{joblerJob: JoblerJobLike, work: (report: JoblerProgressReporter) => Promise<void>}} args
|
|
49
|
+
* @returns {Promise<void>}
|
|
50
|
+
*/
|
|
51
|
+
async run({joblerJob, work}) {
|
|
52
|
+
/** @type {JoblerProgressReporter} */
|
|
53
|
+
const report = {
|
|
54
|
+
progress: async ({current, total, message} = {}) => {
|
|
55
|
+
if (current !== undefined) {
|
|
56
|
+
joblerJob.setProgressCurrent(current)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (total !== undefined) {
|
|
60
|
+
joblerJob.setProgressTotal(total)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (message !== undefined) {
|
|
64
|
+
joblerJob.setStatusMessage(message)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await joblerJob.save()
|
|
68
|
+
},
|
|
69
|
+
message: async (message) => {
|
|
70
|
+
joblerJob.setStatusMessage(message)
|
|
71
|
+
await joblerJob.save()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
joblerJob.setStatus(JOBLER_JOB_STATUS_RUNNING)
|
|
76
|
+
joblerJob.setStartedAt(new Date())
|
|
77
|
+
await joblerJob.save()
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
await work(report)
|
|
81
|
+
|
|
82
|
+
joblerJob.setStatus(JOBLER_JOB_STATUS_SUCCEEDED)
|
|
83
|
+
joblerJob.setFinishedAt(new Date())
|
|
84
|
+
await joblerJob.save()
|
|
85
|
+
} catch (error) {
|
|
86
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error))
|
|
87
|
+
|
|
88
|
+
joblerJob.setStatus(JOBLER_JOB_STATUS_FAILED)
|
|
89
|
+
joblerJob.setErrorMessage(normalizedError.message)
|
|
90
|
+
joblerJob.setFinishedAt(new Date())
|
|
91
|
+
await joblerJob.save()
|
|
92
|
+
|
|
93
|
+
if (this.onError) {
|
|
94
|
+
await this.onError(normalizedError, joblerJob)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
throw normalizedError
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import Migration from "velocious/build/src/database/migration/index.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generic background-job progress records (a jobler-style tracker). A service
|
|
7
|
+
* creates one before starting heavy async work and updates its status/progress as
|
|
8
|
+
* it runs; the Expo UI subscribes to the broadcasting frontend model to show live
|
|
9
|
+
* progress. Lives in the global/default DB only — never a project tenant DB, since
|
|
10
|
+
* a job tracking a tenant-DB teardown cannot live inside the database it drops.
|
|
11
|
+
*/
|
|
12
|
+
export default class CreateJoblerJobs extends Migration {
|
|
13
|
+
/** @returns {Promise<void>} */
|
|
14
|
+
async up() {
|
|
15
|
+
await this.createTable("jobler_jobs", (table) => {
|
|
16
|
+
table.string("name", {null: false})
|
|
17
|
+
table.string("kind", {maxLength: 64, null: false})
|
|
18
|
+
table.string("status", {default: "pending", maxLength: 32, null: false})
|
|
19
|
+
table.integer("progress_current", {default: 0, null: false})
|
|
20
|
+
table.integer("progress_total", {null: true})
|
|
21
|
+
table.string("status_message", {null: true})
|
|
22
|
+
table.text("error_message", {null: true})
|
|
23
|
+
table.text("result_data", {null: true})
|
|
24
|
+
table.datetime("started_at", {null: true})
|
|
25
|
+
table.datetime("finished_at", {null: true})
|
|
26
|
+
table.timestamps()
|
|
27
|
+
})
|
|
28
|
+
await this.addIndex("jobler_jobs", ["kind", "status"], {name: "index_jobler_jobs_on_kind_and_status"})
|
|
29
|
+
await this.addIndex("jobler_jobs", ["created_at"], {name: "index_jobler_jobs_on_created_at"})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** @returns {Promise<void>} */
|
|
33
|
+
async down() {
|
|
34
|
+
await this.dropTable("jobler_jobs")
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/** @import {NamedExoticComponent, ReactNode} from "react" */
|
|
3
|
+
import {memo, useEffect} from "react"
|
|
4
|
+
import PropTypes from "prop-types"
|
|
5
|
+
import {ActivityIndicator, StyleSheet, Text, View} from "react-native"
|
|
6
|
+
import {shapeComponent, ShapeComponent} from "set-state-compare/build/shape-component"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {object} JoblerJobModelLike
|
|
10
|
+
* @property {() => string} status
|
|
11
|
+
* @property {() => number} progressCurrent
|
|
12
|
+
* @property {() => number | null} progressTotal
|
|
13
|
+
* @property {() => string | null} statusMessage
|
|
14
|
+
* @property {() => string | null} errorMessage
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {object} JobProgressIndicatorProps
|
|
19
|
+
* @property {{findBy: (conditions: {id: string}) => Promise<JoblerJobModelLike | null>, onUpdate: (callback: (event: {id: string}) => void) => Promise<() => void>}} model - The jobler-job Velocious frontend-model class.
|
|
20
|
+
* @property {string} joblerJobId - Id of the jobler job to track.
|
|
21
|
+
* @property {(args: {errorMessage: string | null}) => void} [onFailed] - Called once when the job fails.
|
|
22
|
+
* @property {() => void} [onSucceeded] - Called once when the job succeeds.
|
|
23
|
+
* @property {{loading?: string, working?: string, failed?: string}} [labels] - Override the built-in English labels.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {object} JobProgressIndicatorState
|
|
28
|
+
* @property {JoblerJobModelLike | null} joblerJob
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const DEFAULT_LABELS = {failed: "Failed", loading: "Loading…", working: "Working…"}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Live progress for a jobler-tracked background job: loads the job through the given
|
|
35
|
+
* Velocious frontend-model, subscribes to its websocket updates, renders status, a
|
|
36
|
+
* progress bar and any message, and fires onSucceeded/onFailed once on a terminal
|
|
37
|
+
* status. Works with any status-bearing broadcasting frontend model.
|
|
38
|
+
*/
|
|
39
|
+
/** @type {NamedExoticComponent<JobProgressIndicatorProps>} */
|
|
40
|
+
const JobProgressIndicator = memo(shapeComponent(
|
|
41
|
+
/** @augments {ShapeComponent<JobProgressIndicatorProps, JobProgressIndicatorState>} */
|
|
42
|
+
class JobProgressIndicator extends ShapeComponent {
|
|
43
|
+
static defaultProps = {
|
|
44
|
+
labels: undefined,
|
|
45
|
+
onFailed: undefined,
|
|
46
|
+
onSucceeded: undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static propTypes = {
|
|
50
|
+
joblerJobId: PropTypes.string.isRequired,
|
|
51
|
+
labels: PropTypes.object,
|
|
52
|
+
model: PropTypes.any.isRequired,
|
|
53
|
+
onFailed: PropTypes.func,
|
|
54
|
+
onSucceeded: PropTypes.func
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** @type {JobProgressIndicatorState} */
|
|
58
|
+
state = {
|
|
59
|
+
joblerJob: null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** @type {boolean} */
|
|
63
|
+
terminalNotified = false
|
|
64
|
+
|
|
65
|
+
/** @returns {void} */
|
|
66
|
+
setup() {
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
void this.loadData()
|
|
69
|
+
}, [this.p.joblerJobId])
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
let stopped = false
|
|
73
|
+
/** @type {Array<() => void>} */
|
|
74
|
+
const unsubscribers = []
|
|
75
|
+
|
|
76
|
+
void this.subscribe(unsubscribers, () => stopped)
|
|
77
|
+
|
|
78
|
+
return () => {
|
|
79
|
+
stopped = true
|
|
80
|
+
|
|
81
|
+
for (const unsubscribe of unsubscribers) unsubscribe()
|
|
82
|
+
}
|
|
83
|
+
}, [this.p.joblerJobId])
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** @returns {Promise<void>} */
|
|
87
|
+
async loadData() {
|
|
88
|
+
const joblerJob = await this.p.model.findBy({id: this.p.joblerJobId})
|
|
89
|
+
|
|
90
|
+
this.s.joblerJob = joblerJob
|
|
91
|
+
|
|
92
|
+
if (joblerJob) {
|
|
93
|
+
this.notifyTerminalStatus(joblerJob)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {Array<() => void>} unsubscribers
|
|
99
|
+
* @param {() => boolean} stopped
|
|
100
|
+
* @returns {Promise<void>}
|
|
101
|
+
*/
|
|
102
|
+
async subscribe(unsubscribers, stopped) {
|
|
103
|
+
const unsubscribe = await this.p.model.onUpdate(this.onJoblerJobUpdated)
|
|
104
|
+
|
|
105
|
+
if (stopped()) {
|
|
106
|
+
unsubscribe()
|
|
107
|
+
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
unsubscribers.push(unsubscribe)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {{id: string}} event
|
|
116
|
+
* @returns {void}
|
|
117
|
+
*/
|
|
118
|
+
onJoblerJobUpdated = ({id}) => {
|
|
119
|
+
if (id !== this.p.joblerJobId) {
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
void this.loadData()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {JoblerJobModelLike} joblerJob
|
|
128
|
+
* @returns {void}
|
|
129
|
+
*/
|
|
130
|
+
notifyTerminalStatus(joblerJob) {
|
|
131
|
+
if (this.terminalNotified) {
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const status = joblerJob.status()
|
|
136
|
+
|
|
137
|
+
if (status === "succeeded") {
|
|
138
|
+
this.terminalNotified = true
|
|
139
|
+
|
|
140
|
+
if (this.p.onSucceeded) {
|
|
141
|
+
this.p.onSucceeded()
|
|
142
|
+
}
|
|
143
|
+
} else if (status === "failed") {
|
|
144
|
+
this.terminalNotified = true
|
|
145
|
+
|
|
146
|
+
if (this.p.onFailed) {
|
|
147
|
+
this.p.onFailed({errorMessage: joblerJob.errorMessage()})
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** @returns {ReactNode} */
|
|
153
|
+
render() {
|
|
154
|
+
const labels = {...DEFAULT_LABELS, ...this.p.labels}
|
|
155
|
+
const joblerJob = this.s.joblerJob
|
|
156
|
+
|
|
157
|
+
if (!joblerJob) {
|
|
158
|
+
return (
|
|
159
|
+
<View style={styles.row}>
|
|
160
|
+
<ActivityIndicator color="#93c5fd" />
|
|
161
|
+
<Text style={styles.message}>{labels.loading}</Text>
|
|
162
|
+
</View>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const status = joblerJob.status()
|
|
167
|
+
const inProgress = status === "pending" || status === "running"
|
|
168
|
+
const failed = status === "failed"
|
|
169
|
+
const progressTotal = joblerJob.progressTotal()
|
|
170
|
+
const statusMessage = joblerJob.statusMessage()
|
|
171
|
+
const jobErrorMessage = joblerJob.errorMessage()
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<View style={styles.wrap}>
|
|
175
|
+
<View style={styles.row}>
|
|
176
|
+
{inProgress && <ActivityIndicator color="#93c5fd" />}
|
|
177
|
+
<Text style={failed ? styles.statusFailed : styles.status}>
|
|
178
|
+
{failed ? labels.failed : (statusMessage || labels.working)}
|
|
179
|
+
</Text>
|
|
180
|
+
</View>
|
|
181
|
+
{progressTotal !== null && progressTotal > 0 && (
|
|
182
|
+
<View style={styles.progressTrack}>
|
|
183
|
+
<View style={[styles.progressFill, {width: /** @type {import("react-native").DimensionValue} */ (`${Math.min(100, Math.round((joblerJob.progressCurrent() / progressTotal) * 100))}%`)}]} />
|
|
184
|
+
</View>
|
|
185
|
+
)}
|
|
186
|
+
{failed && Boolean(jobErrorMessage) && <Text style={styles.error}>{jobErrorMessage}</Text>}
|
|
187
|
+
</View>
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
))
|
|
192
|
+
|
|
193
|
+
const styles = StyleSheet.create({
|
|
194
|
+
error: {color: "#fca5a5"},
|
|
195
|
+
message: {color: "#cbd5f5"},
|
|
196
|
+
progressFill: {backgroundColor: "#3b82f6", height: 6},
|
|
197
|
+
progressTrack: {backgroundColor: "#1e293b", borderRadius: 4, height: 6, overflow: "hidden"},
|
|
198
|
+
row: {alignItems: "center", flexDirection: "row", gap: 8},
|
|
199
|
+
status: {color: "#cbd5f5", fontWeight: "600"},
|
|
200
|
+
statusFailed: {color: "#fca5a5", fontWeight: "600"},
|
|
201
|
+
wrap: {gap: 8}
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
JobProgressIndicator.displayName = "JobProgressIndicator"
|
|
205
|
+
|
|
206
|
+
export default JobProgressIndicator
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// This file is auto-generated by Velocious. Do not edit it manually — any changes
|
|
2
|
+
// will be overwritten the next time it is generated. Run `velocious generate:base-models` to regenerate.
|
|
3
|
+
import DatabaseRecord from "velocious/build/src/database/record/index.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Attributes accepted when creating or updating JoblerJob records.
|
|
7
|
+
* @typedef {object} JoblerJobWriteAttributes
|
|
8
|
+
* @property {string} [id] - Value for the id attribute.
|
|
9
|
+
* @property {string} [name] - Value for the name attribute.
|
|
10
|
+
* @property {string} [kind] - Value for the kind attribute.
|
|
11
|
+
* @property {string} [status] - Value for the status attribute.
|
|
12
|
+
* @property {number} [progressCurrent] - Value for the progressCurrent attribute.
|
|
13
|
+
* @property {number | null} [progressTotal] - Value for the progressTotal attribute.
|
|
14
|
+
* @property {string | null} [statusMessage] - Value for the statusMessage attribute.
|
|
15
|
+
* @property {string | null} [errorMessage] - Value for the errorMessage attribute.
|
|
16
|
+
* @property {string | null} [resultData] - Value for the resultData attribute.
|
|
17
|
+
* @property {Date | string | null} [startedAt] - Value for the startedAt attribute.
|
|
18
|
+
* @property {Date | string | null} [finishedAt] - Value for the finishedAt attribute.
|
|
19
|
+
* @property {Date | string | null} [createdAt] - Value for the createdAt attribute.
|
|
20
|
+
* @property {Date | string | null} [updatedAt] - Value for the updatedAt attribute.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** @augments {DatabaseRecord<JoblerJobWriteAttributes>} */
|
|
24
|
+
export default class JoblerJobBase extends DatabaseRecord {
|
|
25
|
+
/**
|
|
26
|
+
* @returns {typeof import("../models/jobler-job.js").default}
|
|
27
|
+
*/
|
|
28
|
+
// @ts-ignore - override narrows return type for better IntelliSense in generated model bases
|
|
29
|
+
getModelClass() { return /** @type {typeof import("../models/jobler-job.js").default} */ (this.constructor) }
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
id() { return this.readAttribute("id") }
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} newValue
|
|
38
|
+
* @returns {void}
|
|
39
|
+
*/
|
|
40
|
+
setId(newValue) { return this._setColumnAttribute("id", newValue) }
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @returns {boolean}
|
|
44
|
+
*/
|
|
45
|
+
hasId() { return this._hasAttribute(this.id()) }
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
50
|
+
name() { return this.readAttribute("name") }
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {string} newValue
|
|
54
|
+
* @returns {void}
|
|
55
|
+
*/
|
|
56
|
+
setName(newValue) { return this._setColumnAttribute("name", newValue) }
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @returns {boolean}
|
|
60
|
+
*/
|
|
61
|
+
hasName() { return this._hasAttribute(this.name()) }
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @returns {string}
|
|
65
|
+
*/
|
|
66
|
+
kind() { return this.readAttribute("kind") }
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {string} newValue
|
|
70
|
+
* @returns {void}
|
|
71
|
+
*/
|
|
72
|
+
setKind(newValue) { return this._setColumnAttribute("kind", newValue) }
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @returns {boolean}
|
|
76
|
+
*/
|
|
77
|
+
hasKind() { return this._hasAttribute(this.kind()) }
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @returns {string}
|
|
81
|
+
*/
|
|
82
|
+
status() { return this.readAttribute("status") }
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {string} newValue
|
|
86
|
+
* @returns {void}
|
|
87
|
+
*/
|
|
88
|
+
setStatus(newValue) { return this._setColumnAttribute("status", newValue) }
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @returns {boolean}
|
|
92
|
+
*/
|
|
93
|
+
hasStatus() { return this._hasAttribute(this.status()) }
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @returns {number}
|
|
97
|
+
*/
|
|
98
|
+
progressCurrent() { return this.readAttribute("progressCurrent") }
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {number} newValue
|
|
102
|
+
* @returns {void}
|
|
103
|
+
*/
|
|
104
|
+
setProgressCurrent(newValue) { return this._setColumnAttribute("progressCurrent", newValue) }
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @returns {boolean}
|
|
108
|
+
*/
|
|
109
|
+
hasProgressCurrent() { return this._hasAttribute(this.progressCurrent()) }
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @returns {number | null}
|
|
113
|
+
*/
|
|
114
|
+
progressTotal() { return this.readAttribute("progressTotal") }
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @param {number | null} newValue
|
|
118
|
+
* @returns {void}
|
|
119
|
+
*/
|
|
120
|
+
setProgressTotal(newValue) { return this._setColumnAttribute("progressTotal", newValue) }
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @returns {boolean}
|
|
124
|
+
*/
|
|
125
|
+
hasProgressTotal() { return this._hasAttribute(this.progressTotal()) }
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @returns {string | null}
|
|
129
|
+
*/
|
|
130
|
+
statusMessage() { return this.readAttribute("statusMessage") }
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* @param {string | null} newValue
|
|
134
|
+
* @returns {void}
|
|
135
|
+
*/
|
|
136
|
+
setStatusMessage(newValue) { return this._setColumnAttribute("statusMessage", newValue) }
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @returns {boolean}
|
|
140
|
+
*/
|
|
141
|
+
hasStatusMessage() { return this._hasAttribute(this.statusMessage()) }
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @returns {string | null}
|
|
145
|
+
*/
|
|
146
|
+
errorMessage() { return this.readAttribute("errorMessage") }
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @param {string | null} newValue
|
|
150
|
+
* @returns {void}
|
|
151
|
+
*/
|
|
152
|
+
setErrorMessage(newValue) { return this._setColumnAttribute("errorMessage", newValue) }
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @returns {boolean}
|
|
156
|
+
*/
|
|
157
|
+
hasErrorMessage() { return this._hasAttribute(this.errorMessage()) }
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @returns {string | null}
|
|
161
|
+
*/
|
|
162
|
+
resultData() { return this.readAttribute("resultData") }
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @param {string | null} newValue
|
|
166
|
+
* @returns {void}
|
|
167
|
+
*/
|
|
168
|
+
setResultData(newValue) { return this._setColumnAttribute("resultData", newValue) }
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @returns {boolean}
|
|
172
|
+
*/
|
|
173
|
+
hasResultData() { return this._hasAttribute(this.resultData()) }
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @returns {Date | null}
|
|
177
|
+
*/
|
|
178
|
+
startedAt() { return this.readAttribute("startedAt") }
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @param {Date | string | null} newValue
|
|
182
|
+
* @returns {void}
|
|
183
|
+
*/
|
|
184
|
+
setStartedAt(newValue) { return this._setColumnAttribute("startedAt", newValue) }
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* @returns {boolean}
|
|
188
|
+
*/
|
|
189
|
+
hasStartedAt() { return this._hasAttribute(this.startedAt()) }
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* @returns {Date | null}
|
|
193
|
+
*/
|
|
194
|
+
finishedAt() { return this.readAttribute("finishedAt") }
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* @param {Date | string | null} newValue
|
|
198
|
+
* @returns {void}
|
|
199
|
+
*/
|
|
200
|
+
setFinishedAt(newValue) { return this._setColumnAttribute("finishedAt", newValue) }
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* @returns {boolean}
|
|
204
|
+
*/
|
|
205
|
+
hasFinishedAt() { return this._hasAttribute(this.finishedAt()) }
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* @returns {Date | null}
|
|
209
|
+
*/
|
|
210
|
+
createdAt() { return this.readAttribute("createdAt") }
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* @param {Date | string | null} newValue
|
|
214
|
+
* @returns {void}
|
|
215
|
+
*/
|
|
216
|
+
setCreatedAt(newValue) { return this._setColumnAttribute("createdAt", newValue) }
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* @returns {boolean}
|
|
220
|
+
*/
|
|
221
|
+
hasCreatedAt() { return this._hasAttribute(this.createdAt()) }
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @returns {Date | null}
|
|
225
|
+
*/
|
|
226
|
+
updatedAt() { return this.readAttribute("updatedAt") }
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* @param {Date | string | null} newValue
|
|
230
|
+
* @returns {void}
|
|
231
|
+
*/
|
|
232
|
+
setUpdatedAt(newValue) { return this._setColumnAttribute("updatedAt", newValue) }
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* @returns {boolean}
|
|
236
|
+
*/
|
|
237
|
+
hasUpdatedAt() { return this._hasAttribute(this.updatedAt()) }
|
|
238
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import JoblerJobBase from "../model-bases/jobler-job.js"
|
|
4
|
+
|
|
5
|
+
export const JOBLER_JOB_STATUS_PENDING = "pending"
|
|
6
|
+
export const JOBLER_JOB_STATUS_RUNNING = "running"
|
|
7
|
+
export const JOBLER_JOB_STATUS_SUCCEEDED = "succeeded"
|
|
8
|
+
export const JOBLER_JOB_STATUS_FAILED = "failed"
|
|
9
|
+
export const JOBLER_JOB_STATUSES = [
|
|
10
|
+
JOBLER_JOB_STATUS_PENDING,
|
|
11
|
+
JOBLER_JOB_STATUS_RUNNING,
|
|
12
|
+
JOBLER_JOB_STATUS_SUCCEEDED,
|
|
13
|
+
JOBLER_JOB_STATUS_FAILED
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generic background-job progress record (a jobler-style tracker). Services create
|
|
18
|
+
* one before heavy async work and update its status/progress as it runs; the Expo
|
|
19
|
+
* UI subscribes to the broadcasting frontend model to show live progress.
|
|
20
|
+
*/
|
|
21
|
+
export default class JoblerJob extends JoblerJobBase {
|
|
22
|
+
/** @returns {void} */
|
|
23
|
+
beforeValidation() {
|
|
24
|
+
if (!this.hasStatus()) {
|
|
25
|
+
this.setStatus(JOBLER_JOB_STATUS_PENDING)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!this.hasProgressCurrent()) {
|
|
29
|
+
this.setProgressCurrent(0)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
JoblerJob.validates("name", {presence: true})
|
|
35
|
+
JoblerJob.validates("kind", {presence: true})
|
|
36
|
+
JoblerJob.validates("status", {presence: true})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import FrontendModelBaseResource from "velocious/build/src/frontend-model-resource/base-resource.js"
|
|
4
|
+
import JoblerJob from "../models/jobler-job.js"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* JoblerJobResource — read-only. Only backend services create and update jobler
|
|
8
|
+
* jobs; clients just read them (and subscribe to live websocket updates). Only the
|
|
9
|
+
* `find` member command is exposed, so jobs can only be looked up individually by
|
|
10
|
+
* their unguessable uuid id — there is no `index`/list or `create` endpoint.
|
|
11
|
+
* @augments {FrontendModelBaseResource<typeof JoblerJob>}
|
|
12
|
+
*/
|
|
13
|
+
export default class JoblerJobResource extends FrontendModelBaseResource {
|
|
14
|
+
/** @type {typeof JoblerJob} */
|
|
15
|
+
static ModelClass = JoblerJob
|
|
16
|
+
|
|
17
|
+
/** @type {string[]} */
|
|
18
|
+
static attributes = ["id", "name", "kind", "status", "progressCurrent", "progressTotal", "statusMessage", "errorMessage", "resultData", "startedAt", "finishedAt", "createdAt", "updatedAt"]
|
|
19
|
+
|
|
20
|
+
// Explicitly empty: an undefined `builtInCollectionCommands` normalizes to the
|
|
21
|
+
// built-in `index` + `create` defaults, which would expose a list-every-job
|
|
22
|
+
// endpoint (cross-tenant leak) and let clients create jobs. Empty disables both.
|
|
23
|
+
/** @type {string[]} */
|
|
24
|
+
static builtInCollectionCommands = []
|
|
25
|
+
|
|
26
|
+
/** @type {string[]} */
|
|
27
|
+
static builtInMemberCommands = ["find"]
|
|
28
|
+
|
|
29
|
+
/** @returns {void} */
|
|
30
|
+
abilities() {
|
|
31
|
+
this.can(["read"])
|
|
32
|
+
}
|
|
33
|
+
}
|