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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kasper Stöckel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# velocious-jobler
|
|
2
|
+
|
|
3
|
+
Track background-job progress and show it **live in a React Native / Expo UI**, driven by [Velocious](https://github.com/kaspernj/velocious) frontend-model websockets. A [jobler](https://github.com/kaspernj/jobler)-style tracker for the Velocious stack.
|
|
4
|
+
|
|
5
|
+
A background service creates a *jobler job* record, updates its status/progress as it runs, and — because the job is a broadcasting Velocious frontend-model resource — the Expo UI subscribes to it and renders progress as it happens. No polling.
|
|
6
|
+
|
|
7
|
+
> Status: `0.0.1`. Extracted from a working implementation in a real app; the runner and the UI component are framework-light and reusable. The Velocious model, model-base, frontend-model resource and migration are **shipped by the package** as a fully-generic (no-account) data layer — a consuming app registers the package and the framework auto-discovers them.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install velocious-jobler
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Peer dependencies (frontend component only): `react`, `react-native`, `prop-types`, and `set-state-compare` (Velocious's ShapeComponent).
|
|
16
|
+
|
|
17
|
+
## 1. The jobler job data layer (shipped)
|
|
18
|
+
|
|
19
|
+
The package ships a **fully-generic** `JoblerJob` — model, model-base, read-only frontend-model resource and migration — with **no account/owner coupling** (no `account_id`, no `belongsTo`). It is a standalone background-job progress tracker: any authenticated user can `find` one by its unguessable uuid id, and there is **no `index`/list command**, so there is no cross-tenant listing leak. Only your backend services create and mutate jobs; clients just read + subscribe.
|
|
20
|
+
|
|
21
|
+
Columns (global DB — not a tenant DB):
|
|
22
|
+
|
|
23
|
+
| column | type | notes |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| `id` | uuid | primary key |
|
|
26
|
+
| `name` | string | human label, e.g. `"Deleting project Foo"` |
|
|
27
|
+
| `kind` | string | e.g. `"project_destroy"` |
|
|
28
|
+
| `status` | string | `pending` \| `running` \| `succeeded` \| `failed` |
|
|
29
|
+
| `progress_current` | integer | default `0` |
|
|
30
|
+
| `progress_total` | integer, null | `null` = indeterminate |
|
|
31
|
+
| `status_message` | string, null | current step |
|
|
32
|
+
| `error_message` | text, null | set on failure |
|
|
33
|
+
| `result_data` | text/json, null | optional |
|
|
34
|
+
| `started_at` / `finished_at` | datetime, null | |
|
|
35
|
+
| `created_at` / `updated_at` | datetime | timestamps |
|
|
36
|
+
|
|
37
|
+
Register the package in your Velocious app so the framework auto-discovers the model, resource and migration:
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
import joblerPackage from "velocious-jobler/velocious-package"
|
|
41
|
+
|
|
42
|
+
// In your Velocious Configuration:
|
|
43
|
+
new Configuration({
|
|
44
|
+
packages: [joblerPackage]
|
|
45
|
+
// ...your other config
|
|
46
|
+
})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Then generate its frontend model and run the migration:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
velocious generate:frontend-models
|
|
53
|
+
velocious db:migrate
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Because the resource is a broadcasting read-only frontend-model resource, its create/update/destroy events flow over the websocket to subscribed clients.
|
|
57
|
+
|
|
58
|
+
## 2. Backend: run work with live progress
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
import JoblerJobRunner from "velocious-jobler/backend/jobler-job-runner"
|
|
62
|
+
|
|
63
|
+
await new JoblerJobRunner({
|
|
64
|
+
// optional: route failures to your bug reporter
|
|
65
|
+
onError: (error, joblerJob) => bugReporter.reportError({error, parameters: {joblerJobId: joblerJob.id()}})
|
|
66
|
+
}).run({
|
|
67
|
+
joblerJob,
|
|
68
|
+
work: async (report) => {
|
|
69
|
+
await report.progress({current: 0, total: 3, message: "Dropping database"})
|
|
70
|
+
await dropDatabase()
|
|
71
|
+
await report.progress({current: 1, total: 3, message: "Cleaning up"})
|
|
72
|
+
await cleanup()
|
|
73
|
+
await report.progress({current: 2, total: 3, message: "Finishing"})
|
|
74
|
+
await finish()
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`run(...)` marks the job `running`, saves each `report.progress(...)`/`report.message(...)` update (each `save()` is a websocket push), then marks it `succeeded` — or, on a throw, marks it `failed`, calls `onError`, and rethrows so your background-job retry policy still applies. Dispatch it from a normal Velocious background job.
|
|
80
|
+
|
|
81
|
+
## 3. Frontend: show it in Expo
|
|
82
|
+
|
|
83
|
+
```jsx
|
|
84
|
+
import JobProgressIndicator from "velocious-jobler/frontend/job-progress-indicator"
|
|
85
|
+
import JoblerJob from "@/src/frontend-models/jobler-job.js"
|
|
86
|
+
|
|
87
|
+
<JobProgressIndicator
|
|
88
|
+
model={JoblerJob}
|
|
89
|
+
joblerJobId={joblerJobId}
|
|
90
|
+
onSucceeded={() => router.replace("/somewhere")}
|
|
91
|
+
onFailed={({errorMessage}) => FlashNotifications.error(errorMessage)}
|
|
92
|
+
/>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
It loads the job through `model.findBy({id})`, subscribes with `model.onUpdate(...)`, renders status + a progress bar + the message, and fires `onSucceeded` / `onFailed` once when the job reaches a terminal status. Override the built-in English labels via the `labels` prop (`{loading, working, failed}`).
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
MIT © Kasper Stöckel
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export const JOBLER_JOB_STATUS_PENDING: "pending";
|
|
2
|
+
export const JOBLER_JOB_STATUS_RUNNING: "running";
|
|
3
|
+
export const JOBLER_JOB_STATUS_SUCCEEDED: "succeeded";
|
|
4
|
+
export const JOBLER_JOB_STATUS_FAILED: "failed";
|
|
5
|
+
/**
|
|
6
|
+
* The subset of a jobler-job model this runner needs. Any Velocious model with a
|
|
7
|
+
* `status` / `progress_current` / `progress_total` / `status_message` /
|
|
8
|
+
* `error_message` / `started_at` / `finished_at` schema satisfies it.
|
|
9
|
+
* @typedef {object} JoblerJobLike
|
|
10
|
+
* @property {(status: string) => void} setStatus
|
|
11
|
+
* @property {(current: number) => void} setProgressCurrent
|
|
12
|
+
* @property {(total: number | null) => void} setProgressTotal
|
|
13
|
+
* @property {(message: string | null) => void} setStatusMessage
|
|
14
|
+
* @property {(message: string | null) => void} setErrorMessage
|
|
15
|
+
* @property {(at: Date) => void} setStartedAt
|
|
16
|
+
* @property {(at: Date) => void} setFinishedAt
|
|
17
|
+
* @property {() => Promise<unknown>} save
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {object} JoblerProgressReporter
|
|
21
|
+
* @property {(args: {current?: number, total?: number | null, message?: string | null}) => Promise<void>} progress
|
|
22
|
+
* @property {(message: string) => Promise<void>} message
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Runs a unit of work while keeping a jobler-job's status and progress live. Each
|
|
26
|
+
* update is a `save()`, which — because the job is a broadcasting Velocious
|
|
27
|
+
* frontend-model resource — pushes a websocket update the UI can render as it
|
|
28
|
+
* happens. Framework-agnostic: pass any model that implements {@link JoblerJobLike}
|
|
29
|
+
* and, optionally, an `onError` reporter (e.g. your app's bug reporter).
|
|
30
|
+
*/
|
|
31
|
+
export default class JoblerJobRunner {
|
|
32
|
+
/**
|
|
33
|
+
* @param {{onError?: (error: Error, joblerJob: JoblerJobLike) => (void | Promise<void>)}} [args={}]
|
|
34
|
+
*/
|
|
35
|
+
constructor(args?: {
|
|
36
|
+
onError?: (error: Error, joblerJob: JoblerJobLike) => (void | Promise<void>);
|
|
37
|
+
});
|
|
38
|
+
onError: ((error: Error, joblerJob: JoblerJobLike) => (void | Promise<void>)) | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Marks the job running, runs `work` (handed a progress reporter), then marks it
|
|
41
|
+
* succeeded. On failure it marks the job failed, calls `onError` if provided, and
|
|
42
|
+
* rethrows so the dispatching background job's retry policy still applies.
|
|
43
|
+
* @param {{joblerJob: JoblerJobLike, work: (report: JoblerProgressReporter) => Promise<void>}} args
|
|
44
|
+
* @returns {Promise<void>}
|
|
45
|
+
*/
|
|
46
|
+
run({ joblerJob, work }: {
|
|
47
|
+
joblerJob: JoblerJobLike;
|
|
48
|
+
work: (report: JoblerProgressReporter) => Promise<void>;
|
|
49
|
+
}): Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The subset of a jobler-job model this runner needs. Any Velocious model with a
|
|
53
|
+
* `status` / `progress_current` / `progress_total` / `status_message` /
|
|
54
|
+
* `error_message` / `started_at` / `finished_at` schema satisfies it.
|
|
55
|
+
*/
|
|
56
|
+
export type JoblerJobLike = {
|
|
57
|
+
setStatus: (status: string) => void;
|
|
58
|
+
setProgressCurrent: (current: number) => void;
|
|
59
|
+
setProgressTotal: (total: number | null) => void;
|
|
60
|
+
setStatusMessage: (message: string | null) => void;
|
|
61
|
+
setErrorMessage: (message: string | null) => void;
|
|
62
|
+
setStartedAt: (at: Date) => void;
|
|
63
|
+
setFinishedAt: (at: Date) => void;
|
|
64
|
+
save: () => Promise<unknown>;
|
|
65
|
+
};
|
|
66
|
+
export type JoblerProgressReporter = {
|
|
67
|
+
progress: (args: {
|
|
68
|
+
current?: number;
|
|
69
|
+
total?: number | null;
|
|
70
|
+
message?: string | null;
|
|
71
|
+
}) => Promise<void>;
|
|
72
|
+
message: (message: string) => Promise<void>;
|
|
73
|
+
};
|
|
74
|
+
//# sourceMappingURL=jobler-job-runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jobler-job-runner.d.ts","sourceRoot":"","sources":["../../../src/backend/jobler-job-runner.js"],"names":[],"mappings":"AAEA,wCAAyC,SAAS,CAAA;AAClD,wCAAyC,SAAS,CAAA;AAClD,0CAA2C,WAAW,CAAA;AACtD,uCAAwC,QAAQ,CAAA;AAEhD;;;;;;;;;;;;;GAaG;AAEH;;;;GAIG;AAEH;;;;;;GAMG;AACH;IACE;;OAEG;IACH,mBAFW;QAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,aAAa,KAAK,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;KAAC,EAIxF;IADC,kBAH4B,KAAK,aAAa,aAAa,KAAK,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,cAG3D;IAG7B;;;;;;OAMG;IACH,yBAHW;QAAC,SAAS,EAAE,aAAa,CAAC;QAAC,IAAI,EAAE,CAAC,MAAM,EAAE,sBAAsB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;KAAC,GACjF,OAAO,CAAC,IAAI,CAAC,CAkDzB;CACF;;;;;;;eAvFa,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI;wBACxB,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI;sBACzB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI;sBAC9B,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI;qBAChC,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI;kBAChC,CAAC,EAAE,EAAE,IAAI,KAAK,IAAI;mBAClB,CAAC,EAAE,EAAE,IAAI,KAAK,IAAI;UAClB,MAAM,OAAO,CAAC,OAAO,CAAC;;;cAKtB,CAAC,IAAI,EAAE;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAC,KAAK,OAAO,CAAC,IAAI,CAAC;aAC3F,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
export const JOBLER_JOB_STATUS_PENDING = "pending";
|
|
3
|
+
export const JOBLER_JOB_STATUS_RUNNING = "running";
|
|
4
|
+
export const JOBLER_JOB_STATUS_SUCCEEDED = "succeeded";
|
|
5
|
+
export const JOBLER_JOB_STATUS_FAILED = "failed";
|
|
6
|
+
/**
|
|
7
|
+
* The subset of a jobler-job model this runner needs. Any Velocious model with a
|
|
8
|
+
* `status` / `progress_current` / `progress_total` / `status_message` /
|
|
9
|
+
* `error_message` / `started_at` / `finished_at` schema satisfies it.
|
|
10
|
+
* @typedef {object} JoblerJobLike
|
|
11
|
+
* @property {(status: string) => void} setStatus
|
|
12
|
+
* @property {(current: number) => void} setProgressCurrent
|
|
13
|
+
* @property {(total: number | null) => void} setProgressTotal
|
|
14
|
+
* @property {(message: string | null) => void} setStatusMessage
|
|
15
|
+
* @property {(message: string | null) => void} setErrorMessage
|
|
16
|
+
* @property {(at: Date) => void} setStartedAt
|
|
17
|
+
* @property {(at: Date) => void} setFinishedAt
|
|
18
|
+
* @property {() => Promise<unknown>} save
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {object} JoblerProgressReporter
|
|
22
|
+
* @property {(args: {current?: number, total?: number | null, message?: string | null}) => Promise<void>} progress
|
|
23
|
+
* @property {(message: string) => Promise<void>} message
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Runs a unit of work while keeping a jobler-job's status and progress live. Each
|
|
27
|
+
* update is a `save()`, which — because the job is a broadcasting Velocious
|
|
28
|
+
* frontend-model resource — pushes a websocket update the UI can render as it
|
|
29
|
+
* happens. Framework-agnostic: pass any model that implements {@link JoblerJobLike}
|
|
30
|
+
* and, optionally, an `onError` reporter (e.g. your app's bug reporter).
|
|
31
|
+
*/
|
|
32
|
+
export default class JoblerJobRunner {
|
|
33
|
+
/**
|
|
34
|
+
* @param {{onError?: (error: Error, joblerJob: JoblerJobLike) => (void | Promise<void>)}} [args={}]
|
|
35
|
+
*/
|
|
36
|
+
constructor(args = {}) {
|
|
37
|
+
this.onError = args.onError;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Marks the job running, runs `work` (handed a progress reporter), then marks it
|
|
41
|
+
* succeeded. On failure it marks the job failed, calls `onError` if provided, and
|
|
42
|
+
* rethrows so the dispatching background job's retry policy still applies.
|
|
43
|
+
* @param {{joblerJob: JoblerJobLike, work: (report: JoblerProgressReporter) => Promise<void>}} args
|
|
44
|
+
* @returns {Promise<void>}
|
|
45
|
+
*/
|
|
46
|
+
async run({ joblerJob, work }) {
|
|
47
|
+
/** @type {JoblerProgressReporter} */
|
|
48
|
+
const report = {
|
|
49
|
+
progress: async ({ current, total, message } = {}) => {
|
|
50
|
+
if (current !== undefined) {
|
|
51
|
+
joblerJob.setProgressCurrent(current);
|
|
52
|
+
}
|
|
53
|
+
if (total !== undefined) {
|
|
54
|
+
joblerJob.setProgressTotal(total);
|
|
55
|
+
}
|
|
56
|
+
if (message !== undefined) {
|
|
57
|
+
joblerJob.setStatusMessage(message);
|
|
58
|
+
}
|
|
59
|
+
await joblerJob.save();
|
|
60
|
+
},
|
|
61
|
+
message: async (message) => {
|
|
62
|
+
joblerJob.setStatusMessage(message);
|
|
63
|
+
await joblerJob.save();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
joblerJob.setStatus(JOBLER_JOB_STATUS_RUNNING);
|
|
67
|
+
joblerJob.setStartedAt(new Date());
|
|
68
|
+
await joblerJob.save();
|
|
69
|
+
try {
|
|
70
|
+
await work(report);
|
|
71
|
+
joblerJob.setStatus(JOBLER_JOB_STATUS_SUCCEEDED);
|
|
72
|
+
joblerJob.setFinishedAt(new Date());
|
|
73
|
+
await joblerJob.save();
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
77
|
+
joblerJob.setStatus(JOBLER_JOB_STATUS_FAILED);
|
|
78
|
+
joblerJob.setErrorMessage(normalizedError.message);
|
|
79
|
+
joblerJob.setFinishedAt(new Date());
|
|
80
|
+
await joblerJob.save();
|
|
81
|
+
if (this.onError) {
|
|
82
|
+
await this.onError(normalizedError, joblerJob);
|
|
83
|
+
}
|
|
84
|
+
throw normalizedError;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic background-job progress records (a jobler-style tracker). A service
|
|
3
|
+
* creates one before starting heavy async work and updates its status/progress as
|
|
4
|
+
* it runs; the Expo UI subscribes to the broadcasting frontend model to show live
|
|
5
|
+
* progress. Lives in the global/default DB only — never a project tenant DB, since
|
|
6
|
+
* a job tracking a tenant-DB teardown cannot live inside the database it drops.
|
|
7
|
+
*/
|
|
8
|
+
export default class CreateJoblerJobs extends Migration {
|
|
9
|
+
}
|
|
10
|
+
import Migration from "velocious/build/src/database/migration/index.js";
|
|
11
|
+
//# sourceMappingURL=20260630140000-create-jobler-jobs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260630140000-create-jobler-jobs.d.ts","sourceRoot":"","sources":["../../../../src/database/migrations/20260630140000-create-jobler-jobs.js"],"names":[],"mappings":"AAIA;;;;;;GAMG;AACH;CAwBC;sBAjCqB,iDAAiD"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import Migration from "velocious/build/src/database/migration/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Generic background-job progress records (a jobler-style tracker). A service
|
|
5
|
+
* creates one before starting heavy async work and updates its status/progress as
|
|
6
|
+
* it runs; the Expo UI subscribes to the broadcasting frontend model to show live
|
|
7
|
+
* progress. Lives in the global/default DB only — never a project tenant DB, since
|
|
8
|
+
* a job tracking a tenant-DB teardown cannot live inside the database it drops.
|
|
9
|
+
*/
|
|
10
|
+
export default class CreateJoblerJobs extends Migration {
|
|
11
|
+
/** @returns {Promise<void>} */
|
|
12
|
+
async up() {
|
|
13
|
+
await this.createTable("jobler_jobs", (table) => {
|
|
14
|
+
table.string("name", { null: false });
|
|
15
|
+
table.string("kind", { maxLength: 64, null: false });
|
|
16
|
+
table.string("status", { default: "pending", maxLength: 32, null: false });
|
|
17
|
+
table.integer("progress_current", { default: 0, null: false });
|
|
18
|
+
table.integer("progress_total", { null: true });
|
|
19
|
+
table.string("status_message", { null: true });
|
|
20
|
+
table.text("error_message", { null: true });
|
|
21
|
+
table.text("result_data", { null: true });
|
|
22
|
+
table.datetime("started_at", { null: true });
|
|
23
|
+
table.datetime("finished_at", { null: true });
|
|
24
|
+
table.timestamps();
|
|
25
|
+
});
|
|
26
|
+
await this.addIndex("jobler_jobs", ["kind", "status"], { name: "index_jobler_jobs_on_kind_and_status" });
|
|
27
|
+
await this.addIndex("jobler_jobs", ["created_at"], { name: "index_jobler_jobs_on_created_at" });
|
|
28
|
+
}
|
|
29
|
+
/** @returns {Promise<void>} */
|
|
30
|
+
async down() {
|
|
31
|
+
await this.dropTable("jobler_jobs");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export default JobProgressIndicator;
|
|
2
|
+
export type JoblerJobModelLike = {
|
|
3
|
+
status: () => string;
|
|
4
|
+
progressCurrent: () => number;
|
|
5
|
+
progressTotal: () => number | null;
|
|
6
|
+
statusMessage: () => string | null;
|
|
7
|
+
errorMessage: () => string | null;
|
|
8
|
+
};
|
|
9
|
+
export type JobProgressIndicatorProps = {
|
|
10
|
+
/**
|
|
11
|
+
* - The jobler-job Velocious frontend-model class.
|
|
12
|
+
*/
|
|
13
|
+
model: {
|
|
14
|
+
findBy: (conditions: {
|
|
15
|
+
id: string;
|
|
16
|
+
}) => Promise<JoblerJobModelLike | null>;
|
|
17
|
+
onUpdate: (callback: (event: {
|
|
18
|
+
id: string;
|
|
19
|
+
}) => void) => Promise<() => void>;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* - Id of the jobler job to track.
|
|
23
|
+
*/
|
|
24
|
+
joblerJobId: string;
|
|
25
|
+
/**
|
|
26
|
+
* - Called once when the job fails.
|
|
27
|
+
*/
|
|
28
|
+
onFailed?: ((args: {
|
|
29
|
+
errorMessage: string | null;
|
|
30
|
+
}) => void) | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* - Called once when the job succeeds.
|
|
33
|
+
*/
|
|
34
|
+
onSucceeded?: (() => void) | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* - Override the built-in English labels.
|
|
37
|
+
*/
|
|
38
|
+
labels?: {
|
|
39
|
+
loading?: string;
|
|
40
|
+
working?: string;
|
|
41
|
+
failed?: string;
|
|
42
|
+
} | undefined;
|
|
43
|
+
};
|
|
44
|
+
export type JobProgressIndicatorState = {
|
|
45
|
+
joblerJob: JoblerJobModelLike | null;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Live progress for a jobler-tracked background job: loads the job through the given
|
|
49
|
+
* Velocious frontend-model, subscribes to its websocket updates, renders status, a
|
|
50
|
+
* progress bar and any message, and fires onSucceeded/onFailed once on a terminal
|
|
51
|
+
* status. Works with any status-bearing broadcasting frontend model.
|
|
52
|
+
*/
|
|
53
|
+
/** @type {NamedExoticComponent<JobProgressIndicatorProps>} */
|
|
54
|
+
declare const JobProgressIndicator: NamedExoticComponent<JobProgressIndicatorProps>;
|
|
55
|
+
import type { NamedExoticComponent } from "react";
|
|
56
|
+
//# sourceMappingURL=job-progress-indicator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"job-progress-indicator.d.ts","sourceRoot":"","sources":["../../../src/frontend/job-progress-indicator.jsx"],"names":[],"mappings":";;YASc,MAAM,MAAM;qBACZ,MAAM,MAAM;mBACZ,MAAM,MAAM,GAAG,IAAI;mBACnB,MAAM,MAAM,GAAG,IAAI;kBACnB,MAAM,MAAM,GAAG,IAAI;;;;;;WAKnB;QAAC,MAAM,EAAE,CAAC,UAAU,EAAE;YAAC,EAAE,EAAE,MAAM,CAAA;SAAC,KAAK,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC;QAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE;YAAC,EAAE,EAAE,MAAM,CAAA;SAAC,KAAK,IAAI,KAAK,OAAO,CAAC,MAAM,IAAI,CAAC,CAAA;KAAC;;;;iBACtJ,MAAM;;;;uBACC;QAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;KAAC,KAAK,IAAI;;;;yBACvC,IAAI;;;;;kBACC,MAAM;kBAAY,MAAM;iBAAW,MAAM;;;;eAKpD,kBAAkB,GAAG,IAAI;;AAKvC;;;;;GAKG;AACH,8DAA8D;AAC9D,oCADW,qBAAqB,yBAAyB,CAAC,CAwJxD;0CA7LiD,OAAO"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// @ts-check
|
|
3
|
+
/** @import {NamedExoticComponent, ReactNode} from "react" */
|
|
4
|
+
import { memo, useEffect } from "react";
|
|
5
|
+
import PropTypes from "prop-types";
|
|
6
|
+
import { ActivityIndicator, StyleSheet, Text, View } from "react-native";
|
|
7
|
+
import { shapeComponent, ShapeComponent } from "set-state-compare/build/shape-component";
|
|
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
|
+
* @typedef {object} JobProgressIndicatorProps
|
|
18
|
+
* @property {{findBy: (conditions: {id: string}) => Promise<JoblerJobModelLike | null>, onUpdate: (callback: (event: {id: string}) => void) => Promise<() => void>}} model - The jobler-job Velocious frontend-model class.
|
|
19
|
+
* @property {string} joblerJobId - Id of the jobler job to track.
|
|
20
|
+
* @property {(args: {errorMessage: string | null}) => void} [onFailed] - Called once when the job fails.
|
|
21
|
+
* @property {() => void} [onSucceeded] - Called once when the job succeeds.
|
|
22
|
+
* @property {{loading?: string, working?: string, failed?: string}} [labels] - Override the built-in English labels.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {object} JobProgressIndicatorState
|
|
26
|
+
* @property {JoblerJobModelLike | null} joblerJob
|
|
27
|
+
*/
|
|
28
|
+
const DEFAULT_LABELS = { failed: "Failed", loading: "Loading…", working: "Working…" };
|
|
29
|
+
/**
|
|
30
|
+
* Live progress for a jobler-tracked background job: loads the job through the given
|
|
31
|
+
* Velocious frontend-model, subscribes to its websocket updates, renders status, a
|
|
32
|
+
* progress bar and any message, and fires onSucceeded/onFailed once on a terminal
|
|
33
|
+
* status. Works with any status-bearing broadcasting frontend model.
|
|
34
|
+
*/
|
|
35
|
+
/** @type {NamedExoticComponent<JobProgressIndicatorProps>} */
|
|
36
|
+
const JobProgressIndicator = memo(shapeComponent(
|
|
37
|
+
/** @augments {ShapeComponent<JobProgressIndicatorProps, JobProgressIndicatorState>} */
|
|
38
|
+
class JobProgressIndicator extends ShapeComponent {
|
|
39
|
+
static defaultProps = {
|
|
40
|
+
labels: undefined,
|
|
41
|
+
onFailed: undefined,
|
|
42
|
+
onSucceeded: undefined
|
|
43
|
+
};
|
|
44
|
+
static propTypes = {
|
|
45
|
+
joblerJobId: PropTypes.string.isRequired,
|
|
46
|
+
labels: PropTypes.object,
|
|
47
|
+
model: PropTypes.any.isRequired,
|
|
48
|
+
onFailed: PropTypes.func,
|
|
49
|
+
onSucceeded: PropTypes.func
|
|
50
|
+
};
|
|
51
|
+
/** @type {JobProgressIndicatorState} */
|
|
52
|
+
state = {
|
|
53
|
+
joblerJob: null
|
|
54
|
+
};
|
|
55
|
+
/** @type {boolean} */
|
|
56
|
+
terminalNotified = false;
|
|
57
|
+
/** @returns {void} */
|
|
58
|
+
setup() {
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
void this.loadData();
|
|
61
|
+
}, [this.p.joblerJobId]);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
let stopped = false;
|
|
64
|
+
/** @type {Array<() => void>} */
|
|
65
|
+
const unsubscribers = [];
|
|
66
|
+
void this.subscribe(unsubscribers, () => stopped);
|
|
67
|
+
return () => {
|
|
68
|
+
stopped = true;
|
|
69
|
+
for (const unsubscribe of unsubscribers)
|
|
70
|
+
unsubscribe();
|
|
71
|
+
};
|
|
72
|
+
}, [this.p.joblerJobId]);
|
|
73
|
+
}
|
|
74
|
+
/** @returns {Promise<void>} */
|
|
75
|
+
async loadData() {
|
|
76
|
+
const joblerJob = await this.p.model.findBy({ id: this.p.joblerJobId });
|
|
77
|
+
this.s.joblerJob = joblerJob;
|
|
78
|
+
if (joblerJob) {
|
|
79
|
+
this.notifyTerminalStatus(joblerJob);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* @param {Array<() => void>} unsubscribers
|
|
84
|
+
* @param {() => boolean} stopped
|
|
85
|
+
* @returns {Promise<void>}
|
|
86
|
+
*/
|
|
87
|
+
async subscribe(unsubscribers, stopped) {
|
|
88
|
+
const unsubscribe = await this.p.model.onUpdate(this.onJoblerJobUpdated);
|
|
89
|
+
if (stopped()) {
|
|
90
|
+
unsubscribe();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
unsubscribers.push(unsubscribe);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* @param {{id: string}} event
|
|
97
|
+
* @returns {void}
|
|
98
|
+
*/
|
|
99
|
+
onJoblerJobUpdated = ({ id }) => {
|
|
100
|
+
if (id !== this.p.joblerJobId) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
void this.loadData();
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* @param {JoblerJobModelLike} joblerJob
|
|
107
|
+
* @returns {void}
|
|
108
|
+
*/
|
|
109
|
+
notifyTerminalStatus(joblerJob) {
|
|
110
|
+
if (this.terminalNotified) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const status = joblerJob.status();
|
|
114
|
+
if (status === "succeeded") {
|
|
115
|
+
this.terminalNotified = true;
|
|
116
|
+
if (this.p.onSucceeded) {
|
|
117
|
+
this.p.onSucceeded();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else if (status === "failed") {
|
|
121
|
+
this.terminalNotified = true;
|
|
122
|
+
if (this.p.onFailed) {
|
|
123
|
+
this.p.onFailed({ errorMessage: joblerJob.errorMessage() });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** @returns {ReactNode} */
|
|
128
|
+
render() {
|
|
129
|
+
const labels = { ...DEFAULT_LABELS, ...this.p.labels };
|
|
130
|
+
const joblerJob = this.s.joblerJob;
|
|
131
|
+
if (!joblerJob) {
|
|
132
|
+
return (_jsxs(View, { style: styles.row, children: [_jsx(ActivityIndicator, { color: "#93c5fd" }), _jsx(Text, { style: styles.message, children: labels.loading })] }));
|
|
133
|
+
}
|
|
134
|
+
const status = joblerJob.status();
|
|
135
|
+
const inProgress = status === "pending" || status === "running";
|
|
136
|
+
const failed = status === "failed";
|
|
137
|
+
const progressTotal = joblerJob.progressTotal();
|
|
138
|
+
const statusMessage = joblerJob.statusMessage();
|
|
139
|
+
const jobErrorMessage = joblerJob.errorMessage();
|
|
140
|
+
return (_jsxs(View, { style: styles.wrap, children: [_jsxs(View, { style: styles.row, children: [inProgress && _jsx(ActivityIndicator, { color: "#93c5fd" }), _jsx(Text, { style: failed ? styles.statusFailed : styles.status, children: failed ? labels.failed : (statusMessage || labels.working) })] }), progressTotal !== null && progressTotal > 0 && (_jsx(View, { style: styles.progressTrack, children: _jsx(View, { style: [styles.progressFill, { width: /** @type {import("react-native").DimensionValue} */ (`${Math.min(100, Math.round((joblerJob.progressCurrent() / progressTotal) * 100))}%`) }] }) })), failed && Boolean(jobErrorMessage) && _jsx(Text, { style: styles.error, children: jobErrorMessage })] }));
|
|
141
|
+
}
|
|
142
|
+
}));
|
|
143
|
+
const styles = StyleSheet.create({
|
|
144
|
+
error: { color: "#fca5a5" },
|
|
145
|
+
message: { color: "#cbd5f5" },
|
|
146
|
+
progressFill: { backgroundColor: "#3b82f6", height: 6 },
|
|
147
|
+
progressTrack: { backgroundColor: "#1e293b", borderRadius: 4, height: 6, overflow: "hidden" },
|
|
148
|
+
row: { alignItems: "center", flexDirection: "row", gap: 8 },
|
|
149
|
+
status: { color: "#cbd5f5", fontWeight: "600" },
|
|
150
|
+
statusFailed: { color: "#fca5a5", fontWeight: "600" },
|
|
151
|
+
wrap: { gap: 8 }
|
|
152
|
+
});
|
|
153
|
+
JobProgressIndicator.displayName = "JobProgressIndicator";
|
|
154
|
+
export default JobProgressIndicator;
|