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.
Files changed (31) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -0
  3. package/build/src/backend/jobler-job-runner.d.ts +74 -0
  4. package/build/src/backend/jobler-job-runner.d.ts.map +1 -0
  5. package/build/src/backend/jobler-job-runner.js +87 -0
  6. package/build/src/database/migrations/20260630140000-create-jobler-jobs.d.ts +11 -0
  7. package/build/src/database/migrations/20260630140000-create-jobler-jobs.d.ts.map +1 -0
  8. package/build/src/database/migrations/20260630140000-create-jobler-jobs.js +33 -0
  9. package/build/src/frontend/job-progress-indicator.d.ts +56 -0
  10. package/build/src/frontend/job-progress-indicator.d.ts.map +1 -0
  11. package/build/src/frontend/job-progress-indicator.js +154 -0
  12. package/build/src/model-bases/jobler-job.d.ts +253 -0
  13. package/build/src/model-bases/jobler-job.d.ts.map +1 -0
  14. package/build/src/model-bases/jobler-job.js +197 -0
  15. package/build/src/models/jobler-job.d.ts +16 -0
  16. package/build/src/models/jobler-job.d.ts.map +1 -0
  17. package/build/src/models/jobler-job.js +31 -0
  18. package/build/src/resources/jobler-job-resource.d.ts +21 -0
  19. package/build/src/resources/jobler-job-resource.d.ts.map +1 -0
  20. package/build/src/resources/jobler-job-resource.js +27 -0
  21. package/build/velocious-package.d.ts +4 -0
  22. package/build/velocious-package.d.ts.map +1 -0
  23. package/build/velocious-package.js +3 -0
  24. package/package.json +77 -0
  25. package/src/backend/jobler-job-runner.js +100 -0
  26. package/src/database/migrations/20260630140000-create-jobler-jobs.js +36 -0
  27. package/src/frontend/job-progress-indicator.jsx +206 -0
  28. package/src/model-bases/jobler-job.js +238 -0
  29. package/src/models/jobler-job.js +36 -0
  30. package/src/resources/jobler-job-resource.js +33 -0
  31. 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
+ }
@@ -0,0 +1,5 @@
1
+ // @ts-check
2
+
3
+ import VelociousPackage from "velocious/build/src/packages/velocious-package.js"
4
+
5
+ export default new VelociousPackage({name: "velocious-jobler", url: import.meta.url})