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
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;