@columenlabs/lumen-tasks 0.0.0-stage → 0.1.93

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Columen Labs
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/NOTICE ADDED
@@ -0,0 +1,12 @@
1
+ Lumen
2
+ Copyright (c) 2026 Columen Labs. Licensed under the MIT License (see LICENSE).
3
+
4
+ Lumen draws price and market charts with TradingView Lightweight Charts™
5
+ (https://github.com/tradingview/lightweight-charts), Copyright (c) 2025
6
+ TradingView, Inc., licensed under the Apache License 2.0. Lumen turns off the
7
+ chart's own logo, so per that library's NOTICE, an app that shows these charts
8
+ should link to https://www.tradingview.com/ somewhere a user can find it (an
9
+ About page or footer is enough).
10
+
11
+ The components under packages/ui/src/ui are based on shadcn/ui
12
+ (https://ui.shadcn.com), MIT License, Copyright (c) 2023 shadcn.
package/README.md CHANGED
@@ -1,3 +1,54 @@
1
- # Temporary Holding Version
1
+ # @columenlabs/lumen-tasks
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Lumen's background jobs, server side: a ledger of the work a person started (a search, a sync, a backfill) with its state, progress and result, cooperative cancel, a reaper that fails anything stuck, and the routes the header Tasks list calls. It pairs with `TasksProvider`, `TasksButton` and `useTaskFor` in `@columenlabs/lumen-ui`, which show a ring while something runs, a dot for ended unseen tasks and a toast with Open when one ends. No dependencies beyond `@columenlabs/lumen-ui-types`: `Request`, `Response` and any SQLite, so it runs in a Worker, a Durable Object, Bun or Node.
4
+
5
+ - Source: https://github.com/columenlabs/lumen (`packages/tasks`). MIT.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ bun add @columenlabs/lumen-tasks @columenlabs/lumen-ui-types
11
+ ```
12
+
13
+ Pin both to the same exact version as the other Lumen packages.
14
+
15
+ ## The ledger
16
+
17
+ ```ts
18
+ import { createTasks, doSql } from '@columenlabs/lumen-tasks';
19
+
20
+ const tasks = createTasks({
21
+ db: doSql(ctx.storage.sql), // or d1Sql(env.DB)
22
+ onEnd: async (t) => { /* email or push for long ones */ },
23
+ });
24
+ ```
25
+
26
+ Nothing runs in the ledger. The work runs where its data is (a Durable Object's alarm, a queue, a runner elsewhere) and reports:
27
+
28
+ ```ts
29
+ const t = await tasks.start({
30
+ owner, app: 'jobs', kind: 'search', title: 'Search · Remote roles',
31
+ link: '/feed', ref: search.id, total: 3, unit: 'sources',
32
+ });
33
+ for (const [i, source] of sources.entries()) {
34
+ if ((await tasks.progress(t.id, { done: i })).stop) return; // cancelled or reaped
35
+ await source.run();
36
+ }
37
+ await tasks.finish(t.id, { state: 'done', summary: '14 new jobs' });
38
+ ```
39
+
40
+ - **Cancel is cooperative.** It ends the task at once; the engine's next `progress` answers `stop: true`. Pass `cancellable: false` for work that cannot stop.
41
+ - **Nothing stays running.** A task open past `limitMinutes` (15) is failed on the next read; no cron needed.
42
+ - **Kept 7 days** (`keepDays`), newest first, 100 at most in a list.
43
+ - `byRef(owner, kind, ref)` finds the newest task for one piece of work, so an engine refuses a second run while one is open.
44
+
45
+ ## The routes
46
+
47
+ ```ts
48
+ import { taskRoutes } from '@columenlabs/lumen-tasks';
49
+
50
+ const routes = taskRoutes({ base: '/api/tasks', tasks: () => tasks, owner: (req) => callerId(req) });
51
+ app.all('/api/tasks/*', (c) => routes(c.req.raw));
52
+ ```
53
+
54
+ `GET /`, `GET /:id`, `POST /seen`, `POST /clear`, `POST /:id/clear`, `POST /:id/cancel`. Writes must be same-origin JSON. `tasks` may return a stub that forwards to a Durable Object holding the ledger (`TasksApi`).
@@ -0,0 +1,3 @@
1
+ export { createTasks, d1Sql, doSql, publicTask, type SqlRunner, type StoredTask, type TaskEnd, type Tasks, type TasksOptions, type TaskStart, } from './store.js';
2
+ export { type TaskRoutesOptions, type TasksApi, taskRoutes } from './routes.js';
3
+ export type { Task, TaskList, TaskState } from '@columenlabs/lumen-ui-types/tasks';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ // Lumen's background jobs on the server. The header list and toasts are `TasksProvider` and
2
+ // `TasksButton` in `@columenlabs/lumen-ui`; the shapes both share are
3
+ // `@columenlabs/lumen-ui-types/tasks`.
4
+ export { createTasks, d1Sql, doSql, publicTask, } from './store.js';
5
+ export { taskRoutes } from './routes.js';
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The HTTP routes the header Tasks list calls (`TasksProvider` in `@columenlabs/lumen-ui`), as one
3
+ * fetch handler on Web `Request`/`Response`, so it mounts in any router:
4
+ *
5
+ * app.all('/api/tasks/*', (c) => tasks(c.req.raw)); // Hono
6
+ *
7
+ * GET / the caller's list (last 7 days, newest first)
8
+ * GET /:id one task
9
+ * POST /seen marks every ended task seen
10
+ * POST /clear drops every ended task; POST /:id/clear drops one
11
+ * POST /:id/cancel cancels one
12
+ *
13
+ * Every call needs a caller; writes must be same-origin JSON, since a signed-in browser session is
14
+ * a cookie and a foreign page's form or fetch must not reach the ledger.
15
+ */
16
+ import type { Task, TaskList } from '@columenlabs/lumen-ui-types/tasks';
17
+ import type { Tasks } from './store.js';
18
+ type Awaitable<T> = T | Promise<T>;
19
+ /** The ledger calls the routes make: the ledger itself, or a stub that forwards to it (a Durable
20
+ * Object that holds it). */
21
+ export interface TasksApi {
22
+ list(owner: string): Awaitable<TaskList>;
23
+ read(owner: string, id: string): Awaitable<Task | null>;
24
+ cancel(owner: string, id: string): Awaitable<Task | null>;
25
+ seen(owner: string): Awaitable<void>;
26
+ clear(owner: string, id?: string): Awaitable<void>;
27
+ }
28
+ export interface TaskRoutesOptions {
29
+ /** Where the routes are mounted (`/api/tasks`). */
30
+ base: string;
31
+ /** The ledger for this request. */
32
+ tasks: (req: Request) => Awaitable<TasksApi | Tasks>;
33
+ /** The verified caller's owner id (the same one the engines `start` with), or null (403). */
34
+ owner: (req: Request) => Awaitable<string | null>;
35
+ }
36
+ export declare function taskRoutes(opts: TaskRoutesOptions): (req: Request) => Promise<Response>;
37
+ export {};
package/dist/routes.js ADDED
@@ -0,0 +1,54 @@
1
+ const HEADERS = { 'content-type': 'application/json', 'cache-control': 'no-store' };
2
+ const json = (body, status = 200) => new Response(JSON.stringify(body), { status, headers: HEADERS });
3
+ const fail = (error, status) => json({ error }, status);
4
+ export function taskRoutes(opts) {
5
+ const base = opts.base.replace(/\/$/, '');
6
+ return async (req) => {
7
+ const url = new URL(req.url);
8
+ if (!url.pathname.startsWith(base))
9
+ return fail('not_found', 404);
10
+ const parts = url.pathname
11
+ .slice(base.length)
12
+ .split('/')
13
+ .filter(Boolean)
14
+ .map(decodeURIComponent);
15
+ const owner = await opts.owner(req);
16
+ if (!owner)
17
+ return fail('forbidden', 403);
18
+ if (req.method === 'POST') {
19
+ if (req.headers.get('origin') !== url.origin)
20
+ return fail('forbidden_origin', 403);
21
+ if (!(req.headers.get('content-type') ?? '').toLowerCase().startsWith('application/json'))
22
+ return fail('json_only', 415);
23
+ }
24
+ const tasks = await opts.tasks(req);
25
+ const [a, b] = parts;
26
+ if (req.method === 'GET') {
27
+ if (parts.length === 0)
28
+ return json(await tasks.list(owner));
29
+ if (parts.length === 1 && a) {
30
+ const t = await tasks.read(owner, a);
31
+ return t ? json({ task: t }) : fail('not_found', 404);
32
+ }
33
+ }
34
+ if (req.method === 'POST') {
35
+ if (parts.length === 1 && a === 'seen') {
36
+ await tasks.seen(owner);
37
+ return json({ ok: true });
38
+ }
39
+ if (parts.length === 1 && a === 'clear') {
40
+ await tasks.clear(owner);
41
+ return json({ ok: true });
42
+ }
43
+ if (parts.length === 2 && a && b === 'clear') {
44
+ await tasks.clear(owner, a);
45
+ return json({ ok: true });
46
+ }
47
+ if (parts.length === 2 && a && b === 'cancel') {
48
+ const t = await tasks.cancel(owner, a);
49
+ return t ? json({ task: t }) : fail('not_found', 404);
50
+ }
51
+ }
52
+ return fail('not_found', 404);
53
+ };
54
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * The Tasks ledger: one row a background job a person started, in any SQLite (a Durable Object's
3
+ * own through `doSql`, or D1 through `d1Sql`). The engine that does the work reports here
4
+ * (`start`, `progress`, `finish`); the person reads, cancels and clears through `taskRoutes`.
5
+ *
6
+ * Nothing runs here: the work runs where its data is (a Durable Object's alarm, a queue, a runner
7
+ * elsewhere) and only reports. Cancel is cooperative: it ends the task at once, and the engine's
8
+ * next `progress` answers `stop: true`, so it stops between steps. A task open past its limit is
9
+ * failed by the reaper, which runs on every read, so no cron is needed and nothing stays running
10
+ * for good. Ended tasks are kept `keepDays` (7) and then dropped.
11
+ *
12
+ * Table `tasks` (plain rows: a title names what ran, never amounts or private values).
13
+ */
14
+ import type { Task, TaskList } from '@columenlabs/lumen-ui-types/tasks';
15
+ type Row = Record<string, unknown>;
16
+ /** The SQL a ledger needs: the same shape as `lumen-people`'s, so one runner serves both. */
17
+ export interface SqlRunner {
18
+ all(sql: string, ...args: unknown[]): Promise<Row[]>;
19
+ run(sql: string, ...args: unknown[]): Promise<void>;
20
+ }
21
+ /** A Durable Object's SQLite (`ctx.storage.sql`). */
22
+ export declare function doSql(sql: {
23
+ exec(q: string, ...args: unknown[]): {
24
+ toArray(): unknown[];
25
+ };
26
+ }): SqlRunner;
27
+ /** A D1 database binding. */
28
+ export declare function d1Sql(db: {
29
+ prepare(q: string): {
30
+ bind(...a: unknown[]): {
31
+ all(): Promise<{
32
+ results: unknown[];
33
+ }>;
34
+ run(): Promise<unknown>;
35
+ };
36
+ };
37
+ }): SqlRunner;
38
+ /** What an engine gives when it starts a job. */
39
+ export interface TaskStart {
40
+ /** Whose task it is (the app's stable id for the person, such as their email). */
41
+ owner: string;
42
+ /** The app it runs on; the list names it when tasks come from several. */
43
+ app: string;
44
+ /** What kind of work (`search`, `sync`, `backfill`). */
45
+ kind: string;
46
+ /** `Search · Remote roles`: what ran, never amounts or private values. */
47
+ title: string;
48
+ /** The page with the result, a whole address. */
49
+ link?: string | null;
50
+ /** How many steps, in `unit`, when the engine counts. */
51
+ total?: number | null;
52
+ unit?: string | null;
53
+ /** The engine's own id for the work, so a page finds the task its button started. */
54
+ ref?: string | null;
55
+ /** Whether Cancel works for it (the engine checks `progress`'s `stop`). Default true. */
56
+ cancellable?: boolean;
57
+ /** Open longer than this fails it (default 15). */
58
+ limitMinutes?: number;
59
+ /** Starts as running (default) or queued, for work that waits its turn. */
60
+ state?: 'queued' | 'running';
61
+ }
62
+ /** How a job ended. */
63
+ export interface TaskEnd {
64
+ state: 'done' | 'failed';
65
+ /** One line on the result (`14 new jobs`). */
66
+ summary?: string | null;
67
+ /** Why it failed. */
68
+ error?: string | null;
69
+ /** The result's page, when it is only known at the end. */
70
+ link?: string | null;
71
+ }
72
+ /** A task as stored: `owner` and the limit stay on the server. */
73
+ export interface StoredTask extends Task {
74
+ owner: string;
75
+ limit_minutes: number;
76
+ }
77
+ export interface TasksOptions {
78
+ db: SqlRunner;
79
+ /** Days an ended task is kept (default 7). */
80
+ keepDays?: number;
81
+ /** Called once when a task ends (done, failed, cancelled or reaped), to notify the person
82
+ * (email, push) for long ones. Errors are swallowed: a failed notice never fails the job. */
83
+ onEnd?: (task: StoredTask) => Promise<void> | void;
84
+ /** The clock (tests and replays); default `Date.now`. */
85
+ now?: () => number;
86
+ }
87
+ export interface Tasks {
88
+ start(t: TaskStart): Promise<StoredTask>;
89
+ /** Reports progress; `stop` is true once the task is no longer open (cancelled or reaped),
90
+ * so the engine stops between steps. */
91
+ progress(id: string, p: {
92
+ done?: number | null;
93
+ total?: number | null;
94
+ state?: 'running';
95
+ }): Promise<{
96
+ stop: boolean;
97
+ }>;
98
+ /** Ends an open task; an already ended one (cancelled meanwhile) stays as it is and gives null. */
99
+ finish(id: string, end: TaskEnd): Promise<StoredTask | null>;
100
+ get(id: string): Promise<StoredTask | null>;
101
+ /** The person's newest task for one piece of work (`kind` + `ref`). */
102
+ byRef(owner: string, kind: string, ref: string): Promise<StoredTask | null>;
103
+ /** The person's tasks over the kept days, newest first; reaps first. */
104
+ list(owner: string): Promise<TaskList>;
105
+ /** The person's task, or null. */
106
+ read(owner: string, id: string): Promise<Task | null>;
107
+ cancel(owner: string, id: string): Promise<Task | null>;
108
+ /** Marks every ended task of the person seen. */
109
+ seen(owner: string): Promise<void>;
110
+ /** Drops one ended task, or every ended one; open tasks stay. */
111
+ clear(owner: string, id?: string): Promise<void>;
112
+ /** Fails tasks open past their limit and drops old ended ones. */
113
+ reap(): Promise<void>;
114
+ }
115
+ /** What the person sees: the stored task without the owner and the limit. */
116
+ export declare function publicTask(t: StoredTask): Task;
117
+ export declare function createTasks(opts: TasksOptions): Tasks;
118
+ export {};
package/dist/store.js ADDED
@@ -0,0 +1,208 @@
1
+ import { isActive } from '@columenlabs/lumen-ui-types/tasks';
2
+ /** A Durable Object's SQLite (`ctx.storage.sql`). */
3
+ export function doSql(sql) {
4
+ return {
5
+ all: async (q, ...a) => sql.exec(q, ...a).toArray(),
6
+ run: async (q, ...a) => {
7
+ sql.exec(q, ...a);
8
+ },
9
+ };
10
+ }
11
+ /** A D1 database binding. */
12
+ export function d1Sql(db) {
13
+ return {
14
+ all: async (q, ...a) => (await db
15
+ .prepare(q)
16
+ .bind(...a)
17
+ .all()).results,
18
+ run: async (q, ...a) => {
19
+ await db
20
+ .prepare(q)
21
+ .bind(...a)
22
+ .run();
23
+ },
24
+ };
25
+ }
26
+ const DAY_MS = 24 * 60 * 60_000;
27
+ const iso = (v) => (v == null ? null : new Date(Number(v)).toISOString());
28
+ const num = (v) => (v == null ? null : Number(v));
29
+ const str = (v) => (v == null ? null : String(v));
30
+ const STATES = ['queued', 'running', 'done', 'failed', 'cancelled'];
31
+ function toTask(r) {
32
+ const state = STATES.includes(r.state) ? r.state : 'failed';
33
+ return {
34
+ id: String(r.id),
35
+ owner: String(r.owner),
36
+ app: String(r.app),
37
+ kind: String(r.kind),
38
+ title: String(r.title),
39
+ link: str(r.link),
40
+ state,
41
+ done: num(r.done),
42
+ total: num(r.total),
43
+ unit: str(r.unit),
44
+ summary: str(r.summary),
45
+ error: str(r.error),
46
+ ref: str(r.ref),
47
+ cancellable: Number(r.cancellable) === 1 && isActive({ state }),
48
+ created_at: iso(r.created_at) ?? new Date(0).toISOString(),
49
+ started_at: iso(r.started_at),
50
+ finished_at: iso(r.finished_at),
51
+ seen_at: iso(r.seen_at),
52
+ limit_minutes: Number(r.limit_minutes ?? 15),
53
+ };
54
+ }
55
+ /** What the person sees: the stored task without the owner and the limit. */
56
+ export function publicTask(t) {
57
+ const { owner: _o, limit_minutes: _l, ...rest } = t;
58
+ return rest;
59
+ }
60
+ export function createTasks(opts) {
61
+ const { db } = opts;
62
+ const now = opts.now ?? Date.now;
63
+ const keep = (opts.keepDays ?? 7) * DAY_MS;
64
+ let ready = null;
65
+ const init = () => {
66
+ ready ??= (async () => {
67
+ await db.run(`CREATE TABLE IF NOT EXISTS tasks (
68
+ id TEXT PRIMARY KEY, owner TEXT NOT NULL, app TEXT NOT NULL, kind TEXT NOT NULL,
69
+ title TEXT NOT NULL, link TEXT, state TEXT NOT NULL, done REAL, total REAL, unit TEXT,
70
+ summary TEXT, error TEXT, ref TEXT, cancellable INTEGER NOT NULL DEFAULT 1,
71
+ limit_minutes INTEGER NOT NULL DEFAULT 15, created_at INTEGER NOT NULL,
72
+ started_at INTEGER, finished_at INTEGER, seen_at INTEGER)`);
73
+ await db.run('CREATE INDEX IF NOT EXISTS tasks_owner ON tasks (owner, created_at)');
74
+ await db.run('CREATE INDEX IF NOT EXISTS tasks_open ON tasks (state)');
75
+ })().catch((e) => {
76
+ ready = null;
77
+ throw e;
78
+ });
79
+ return ready;
80
+ };
81
+ const one = async (id) => {
82
+ await init();
83
+ const [r] = await db.all('SELECT * FROM tasks WHERE id = ?', id);
84
+ return r ? toTask(r) : null;
85
+ };
86
+ const ended = async (t) => {
87
+ if (!t || !opts.onEnd)
88
+ return;
89
+ try {
90
+ await opts.onEnd(t);
91
+ }
92
+ catch {
93
+ // A notice that fails never fails the job.
94
+ }
95
+ };
96
+ // Ends an open task; gives it when this call ended it (so `onEnd` fires once), else null.
97
+ const end = async (id, state, set) => {
98
+ const before = await one(id);
99
+ if (!before || !isActive(before))
100
+ return null;
101
+ const cols = Object.keys(set);
102
+ await db.run(`UPDATE tasks SET state = ?, finished_at = ?${cols.map((c) => `, ${c} = ?`).join('')}
103
+ WHERE id = ? AND state IN ('queued', 'running')`, state, now(), ...cols.map((c) => set[c] ?? null), id);
104
+ return one(id);
105
+ };
106
+ const tasks = {
107
+ async start(t) {
108
+ await init();
109
+ const id = crypto.randomUUID();
110
+ const at = now();
111
+ const state = t.state ?? 'running';
112
+ await db.run(`INSERT INTO tasks (id, owner, app, kind, title, link, state, done, total, unit, ref,
113
+ cancellable, limit_minutes, created_at, started_at)
114
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, t.owner, t.app, t.kind, t.title, t.link ?? null, state, t.total ? 0 : null, t.total ?? null, t.unit ?? null, t.ref ?? null, t.cancellable === false ? 0 : 1, t.limitMinutes ?? 15, at, state === 'running' ? at : null);
115
+ return (await one(id));
116
+ },
117
+ async progress(id, p) {
118
+ await init();
119
+ const sets = [];
120
+ const args = [];
121
+ if (p.done !== undefined) {
122
+ sets.push('done = ?');
123
+ args.push(p.done);
124
+ }
125
+ if (p.total !== undefined) {
126
+ sets.push('total = ?');
127
+ args.push(p.total);
128
+ }
129
+ if (p.state === 'running') {
130
+ sets.push("state = 'running', started_at = COALESCE(started_at, ?)");
131
+ args.push(now());
132
+ }
133
+ if (sets.length)
134
+ await db.run(`UPDATE tasks SET ${sets.join(', ')} WHERE id = ? AND state IN ('queued', 'running')`, ...args, id);
135
+ const t = await one(id);
136
+ return { stop: !t || !isActive(t) };
137
+ },
138
+ async finish(id, e) {
139
+ await init();
140
+ const t = await end(id, e.state, {
141
+ summary: e.summary ?? null,
142
+ error: e.state === 'failed' ? (e.error ?? 'Failed') : null,
143
+ ...(e.link !== undefined ? { link: e.link } : {}),
144
+ });
145
+ // A done task's progress reads full.
146
+ if (t && e.state === 'done' && t.total != null) {
147
+ await db.run('UPDATE tasks SET done = total WHERE id = ?', id);
148
+ t.done = t.total;
149
+ }
150
+ await ended(t);
151
+ return t;
152
+ },
153
+ get: one,
154
+ async byRef(owner, kind, ref) {
155
+ await init();
156
+ const [r] = await db.all('SELECT * FROM tasks WHERE owner = ? AND kind = ? AND ref = ? ORDER BY created_at DESC LIMIT 1', owner, kind, ref);
157
+ return r ? toTask(r) : null;
158
+ },
159
+ async list(owner) {
160
+ await tasks.reap();
161
+ const rows = await db.all('SELECT * FROM tasks WHERE owner = ? AND created_at >= ? ORDER BY created_at DESC LIMIT 100', owner, now() - keep);
162
+ const list = rows.map(toTask);
163
+ return {
164
+ tasks: list.map(publicTask),
165
+ active: list.filter(isActive).length,
166
+ unseen: list.filter((t) => t.finished_at && !t.seen_at).length,
167
+ };
168
+ },
169
+ async read(owner, id) {
170
+ const t = await one(id);
171
+ return t && t.owner === owner ? publicTask(t) : null;
172
+ },
173
+ async cancel(owner, id) {
174
+ const t = await one(id);
175
+ if (!t || t.owner !== owner)
176
+ return null;
177
+ if (!t.cancellable)
178
+ return publicTask(t);
179
+ const done = await end(id, 'cancelled', { error: 'Cancelled' });
180
+ await ended(done);
181
+ return publicTask(done ?? (await one(id)));
182
+ },
183
+ async seen(owner) {
184
+ await init();
185
+ await db.run('UPDATE tasks SET seen_at = ? WHERE owner = ? AND finished_at IS NOT NULL AND seen_at IS NULL', now(), owner);
186
+ },
187
+ async clear(owner, id) {
188
+ await init();
189
+ if (id)
190
+ await db.run('DELETE FROM tasks WHERE owner = ? AND id = ? AND finished_at IS NOT NULL', owner, id);
191
+ else
192
+ await db.run('DELETE FROM tasks WHERE owner = ? AND finished_at IS NOT NULL', owner);
193
+ },
194
+ async reap() {
195
+ await init();
196
+ const at = now();
197
+ const stuck = await db.all("SELECT id, limit_minutes FROM tasks WHERE state IN ('queued', 'running') AND created_at < ? - limit_minutes * 60000", at);
198
+ for (const r of stuck) {
199
+ const t = await end(String(r.id), 'failed', {
200
+ error: `Stopped after ${Number(r.limit_minutes)} ${Number(r.limit_minutes) === 1 ? 'minute' : 'minutes'}`,
201
+ });
202
+ await ended(t);
203
+ }
204
+ await db.run('DELETE FROM tasks WHERE finished_at IS NOT NULL AND finished_at < ?', at - keep);
205
+ },
206
+ };
207
+ return tasks;
208
+ }
package/package.json CHANGED
@@ -1,6 +1,45 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-tasks",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.93",
4
+ "description": "Lumen's background jobs on the server: a ledger of the work a person started (state, progress, result), cancel, a reaper for stuck jobs and the routes the header Tasks list calls.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./store": {
15
+ "types": "./dist/store.d.ts",
16
+ "default": "./dist/store.js"
17
+ },
18
+ "./routes": {
19
+ "types": "./dist/routes.d.ts",
20
+ "default": "./dist/routes.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "LICENSE",
26
+ "NOTICE"
27
+ ],
28
+ "sideEffects": [
29
+ "**/*.css"
30
+ ],
31
+ "homepage": "https://github.com/columenlabs/lumen",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/columenlabs/lumen.git",
35
+ "directory": "packages/tasks"
36
+ },
37
+ "bugs": "https://github.com/columenlabs/lumen/issues",
38
+ "publishConfig": {
39
+ "access": "public",
40
+ "registry": "https://registry.npmjs.org/"
41
+ },
42
+ "dependencies": {
43
+ "@columenlabs/lumen-ui-types": "0.1.93"
44
+ }
45
+ }