@happyvertical/smrt-jobs 0.30.0
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/AGENTS.md +71 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +7 -0
- package/README.md +151 -0
- package/dist/__smrt-register__.d.ts +2 -0
- package/dist/__smrt-register__.d.ts.map +1 -0
- package/dist/background-policy.d.ts +121 -0
- package/dist/background-policy.d.ts.map +1 -0
- package/dist/chunks/runner-DV8FBO0y.js +1642 -0
- package/dist/chunks/runner-DV8FBO0y.js.map +1 -0
- package/dist/chunks/worker-liveness-DOTjoIjr.js +65 -0
- package/dist/chunks/worker-liveness-DOTjoIjr.js.map +1 -0
- package/dist/error-redaction.d.ts +48 -0
- package/dist/error-redaction.d.ts.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +926 -0
- package/dist/index.js.map +1 -0
- package/dist/job-builder.d.ts +94 -0
- package/dist/job-builder.d.ts.map +1 -0
- package/dist/job-handle.d.ts +71 -0
- package/dist/job-handle.d.ts.map +1 -0
- package/dist/logger-extension.d.ts +58 -0
- package/dist/logger-extension.d.ts.map +1 -0
- package/dist/manifest.json +1327 -0
- package/dist/object-extension.d.ts +68 -0
- package/dist/object-extension.d.ts.map +1 -0
- package/dist/playground.d.ts +2 -0
- package/dist/playground.d.ts.map +1 -0
- package/dist/playground.js +179 -0
- package/dist/playground.js.map +1 -0
- package/dist/runner.d.ts +189 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +15 -0
- package/dist/runner.js.map +1 -0
- package/dist/schedule-runner.d.ts +151 -0
- package/dist/schedule-runner.d.ts.map +1 -0
- package/dist/smrt-job-event.d.ts +54 -0
- package/dist/smrt-job-event.d.ts.map +1 -0
- package/dist/smrt-job.d.ts +215 -0
- package/dist/smrt-job.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +508 -0
- package/dist/smrt-worker.d.ts +72 -0
- package/dist/smrt-worker.d.ts.map +1 -0
- package/dist/stale-recovery.d.ts +34 -0
- package/dist/stale-recovery.d.ts.map +1 -0
- package/dist/svelte/components/JobActions.svelte +103 -0
- package/dist/svelte/components/JobActions.svelte.d.ts +23 -0
- package/dist/svelte/components/JobActions.svelte.d.ts.map +1 -0
- package/dist/svelte/components/JobDashboard.svelte +199 -0
- package/dist/svelte/components/JobDashboard.svelte.d.ts +27 -0
- package/dist/svelte/components/JobDashboard.svelte.d.ts.map +1 -0
- package/dist/svelte/components/JobDetail.svelte +256 -0
- package/dist/svelte/components/JobDetail.svelte.d.ts +17 -0
- package/dist/svelte/components/JobDetail.svelte.d.ts.map +1 -0
- package/dist/svelte/components/JobList.svelte +360 -0
- package/dist/svelte/components/JobList.svelte.d.ts +28 -0
- package/dist/svelte/components/JobList.svelte.d.ts.map +1 -0
- package/dist/svelte/components/JobStats.svelte +242 -0
- package/dist/svelte/components/JobStats.svelte.d.ts +15 -0
- package/dist/svelte/components/JobStats.svelte.d.ts.map +1 -0
- package/dist/svelte/components/JobStatusBadge.svelte +23 -0
- package/dist/svelte/components/JobStatusBadge.svelte.d.ts +9 -0
- package/dist/svelte/components/JobStatusBadge.svelte.d.ts.map +1 -0
- package/dist/svelte/components/types.d.ts +9 -0
- package/dist/svelte/components/types.d.ts.map +1 -0
- package/dist/svelte/components/types.js +8 -0
- package/dist/svelte/i18n.d.ts +22 -0
- package/dist/svelte/i18n.d.ts.map +1 -0
- package/dist/svelte/i18n.js +22 -0
- package/dist/svelte/index.d.ts +25 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +28 -0
- package/dist/svelte/playground.d.ts +329 -0
- package/dist/svelte/playground.d.ts.map +1 -0
- package/dist/svelte/playground.js +174 -0
- package/dist/svelte/types.d.ts +191 -0
- package/dist/svelte/types.d.ts.map +1 -0
- package/dist/svelte/types.js +87 -0
- package/dist/ui.d.ts +10 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +69 -0
- package/dist/ui.js.map +1 -0
- package/dist/worker-liveness-thread.d.ts +2 -0
- package/dist/worker-liveness-thread.d.ts.map +1 -0
- package/dist/worker-liveness-thread.js +66 -0
- package/dist/worker-liveness-thread.js.map +1 -0
- package/dist/worker-liveness-ticker.d.ts +30 -0
- package/dist/worker-liveness-ticker.d.ts.map +1 -0
- package/dist/worker-liveness.d.ts +71 -0
- package/dist/worker-liveness.d.ts.map +1 -0
- package/package.json +93 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# @happyvertical/smrt-jobs
|
|
2
|
+
|
|
3
|
+
Background job execution with persistent queue, scheduling, and fluent builder API.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
SmrtObject.bg('method') → SmrtJob (in _smrt_jobs) → TaskRunner picks up → executes via ObjectRegistry
|
|
9
|
+
AgentSchedule (cron) → ScheduleRunner creates SmrtJob → TaskRunner executes → ScheduleRunner updates
|
|
10
|
+
TaskRunner.start() → SmrtWorker lease (in _smrt_workers) → recovery keys on worker liveness, not heartbeat
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## SmrtJob
|
|
14
|
+
|
|
15
|
+
Persistent in `_smrt_jobs`. Fields: `queue` (default), `objectType`, `objectId`, `method`, `args`, `runAt`, `priority` (higher=sooner), `status`, `attempts`/`maxAttempts`, `timeout` (default 5min), `retryStrategy`, `workerId` (the owning runner's incarnation key), `workerHeartbeat` (telemetry only — no longer gates recovery).
|
|
16
|
+
|
|
17
|
+
Status: `pending → running → completed/failed/cancelled`.
|
|
18
|
+
|
|
19
|
+
## TaskRunner
|
|
20
|
+
|
|
21
|
+
Polling-based execution engine. Config: `concurrency` (5), `pollInterval` (1s), `heartbeatInterval` (30s, telemetry only), `leaseTtlMs` (30s), `leaseTickMs` (10s), `shutdownTimeout` (30s).
|
|
22
|
+
|
|
23
|
+
1. `start()` calls `assertReady()` (fail fast if `_smrt_workers` unmigrated), registers a seeded `SmrtWorker` lease, and adds its worker key to the process-global live set — all **before** polling
|
|
24
|
+
2. Polls `claimReady()` to atomically claim pending jobs (`runAt <= NOW`, ordered by `priority DESC, runAt ASC, created_at ASC, id ASC`)
|
|
25
|
+
3. Claim sets `status='running'`, `workerId=<incarnation key>`, heartbeat/start timestamps, and increments `attempts`
|
|
26
|
+
4. Resolves class via `ObjectRegistry.getClass(objectType)`, creates instance, calls method
|
|
27
|
+
5. **Internal args**: `_agentConfig` and `_scheduleId` stripped from args before calling method
|
|
28
|
+
6. Terminal/retry writes are **conditional** (`WHERE worker_id=? AND status='running'`) so a recovered row is never stomped
|
|
29
|
+
7. Retry: uses strategy from `@happyvertical/jobs`, schedules future `runAt` on failure
|
|
30
|
+
8. Events: `job:started`, `job:completed`, `job:failed`, `job:retrying`, `runner:started/stopped`
|
|
31
|
+
|
|
32
|
+
## Worker liveness & recovery (#1474)
|
|
33
|
+
|
|
34
|
+
Recovery keys on **worker-process liveness**, never per-job heartbeat freshness (a CPU-bound synchronous handler used to starve the heartbeat and false-recover its own running jobs).
|
|
35
|
+
|
|
36
|
+
- **`SmrtWorker` / `_smrt_workers`**: one lease row per runner *incarnation* (`workerId` is per-incarnation unique via `createWorkerKey`, so a restart never looks like it still owns the previous crash's jobs). `leaseExpiresAt` is a `datetime` (an integer epoch-ms column overflows `int4`/`INT32` on Postgres/DuckDB). Stage 1 writes/compares it against the host clock — the same approach the old heartbeat recovery used, so it's no more skew-sensitive than the code it replaced.
|
|
37
|
+
- **Process-global live set** (`worker-liveness.ts`, `globalThis.__smrtLiveWorkers`): checked synchronously, so it can't be starved by a blocked loop. Covers all same-process topologies.
|
|
38
|
+
- **Off-loop ticker** (`worker-liveness-ticker.ts` + `worker-liveness-thread.ts`): for engines a second connection can reach (Postgres, file-backed SQLite — `offLoopEligible()`), `start()` spawns a `node:worker_threads` ticker that renews the lease on its own thread, so a CPU-bound synchronous handler on the main loop can't starve it. In-memory SQLite / DuckDB, a thread-spawn failure, a start-handshake timeout, or the thread dying mid-run all fall back to main-loop renewal; the in-process live set keeps same-process correct regardless. The worker entry is a separate build entry resolved via `import.meta.resolve('@happyvertical/smrt-jobs/worker-liveness-thread')`.
|
|
39
|
+
- **Recovery rule** (both runners): a `running` job is orphaned iff its worker is *not alive* = not in the live set **and** no fresh `_smrt_workers` lease. The live set takes precedence over a stale lease. TaskRunner also never recovers a job in its own `activeJobs`. If `_smrt_workers` is absent, recovery skips lease checks (never mass-recovers). Recovery is swept at most once per lease tick, and terminal/recovery writes use `RETURNING id` (not `rowCount`, which DuckDB/JSON adapters always report as ≥1).
|
|
40
|
+
- **Lease clock**: the lease is compared against the host clock (same as the old heartbeat; fine with NTP + a 30s TTL). A dead process stops renewing and the lease expires within its TTL — that is how recovery detects death. (Instant cross-process detection via Postgres session advisory locks was prototyped on `@happyvertical/sql`'s `acquireSession()` but deferred — treating a free lock as proof-of-death false-recovers any worker legitimately in main-loop fallback mode.)
|
|
41
|
+
|
|
42
|
+
## ScheduleRunner
|
|
43
|
+
|
|
44
|
+
Polls `_smrt_agent_schedules` every 60s for due entries. Creates SmrtJob with `queue='agents'`, `priority=75`. Wires to TaskRunner events for completion/failure tracking. Slot reconciliation keys on worker liveness (it has no in-process active-job set, so the lease/live-set is its whole mechanism).
|
|
45
|
+
|
|
46
|
+
Custom cron parser: 5-field (minute hour dom month dow). `*`, ranges, lists, steps supported. **Not timezone-aware** (UTC).
|
|
47
|
+
|
|
48
|
+
## JobBuilder — Fluent API
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
const handle = await doc.background('analyze', { detailed: true })
|
|
52
|
+
.delay('5m').priority('high').retries(5).queue('analysis').timeout(600000).enqueue();
|
|
53
|
+
|
|
54
|
+
await handle.wait({ timeout: 60000, pollInterval: 100 }); // polling-based
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`bg()` is shorthand: `await doc.bg('analyze', args)` → enqueues immediately, returns JobHandle.
|
|
58
|
+
|
|
59
|
+
## withBackgroundJobs(Class)
|
|
60
|
+
|
|
61
|
+
Mixin that adds `bg()` and `background()` to any SmrtObject. Uses WeakMap for collection caching per DB instance.
|
|
62
|
+
|
|
63
|
+
## Gotchas
|
|
64
|
+
|
|
65
|
+
- **Cron not timezone-aware**: all times treated as UTC
|
|
66
|
+
- **No dead letter queue**: failed jobs stay in DB with `status='failed'` — manual intervention
|
|
67
|
+
- **Result storage**: `resultPointer` is just a string — app must implement result backend
|
|
68
|
+
- **Lazy builder**: `background()` returns builder — nothing happens until `enqueue()`
|
|
69
|
+
- **wait() is polling**: JobHandle.wait() polls DB every 100ms (configurable)
|
|
70
|
+
- **Migrate before start()**: `TaskRunner.start()` throws if `_smrt_workers` is missing — run `smrt db:migrate` after upgrading. Tables are never created at runtime.
|
|
71
|
+
- **Recovery is liveness-based**: don't reintroduce heartbeat-threshold recovery; a blocked event loop must not look dead (see Worker liveness section, #1474)
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright <2025> <Happy Vertical Corporation>
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# @happyvertical/smrt-jobs
|
|
2
|
+
|
|
3
|
+
Background job execution for SMRT objects. Provides persistent queue storage, retry strategies, cron-based scheduling, and a fluent `JobBuilder` API via the `withBackgroundJobs()` mixin.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @happyvertical/smrt-jobs
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Add background capabilities to a SmrtObject
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { withBackgroundJobs, TaskRunner } from '@happyvertical/smrt-jobs';
|
|
17
|
+
import { Document } from './document.js';
|
|
18
|
+
|
|
19
|
+
// Mixin adds .bg() and .background() to any SmrtObject class
|
|
20
|
+
const BackgroundDocument = withBackgroundJobs(Document);
|
|
21
|
+
const doc = new BackgroundDocument({ db });
|
|
22
|
+
await doc.initialize();
|
|
23
|
+
|
|
24
|
+
// Quick enqueue — runs immediately when a TaskRunner picks it up
|
|
25
|
+
const handle = await doc.bg('generateSummary', { format: 'md' });
|
|
26
|
+
|
|
27
|
+
// Fluent builder for advanced options
|
|
28
|
+
const handle2 = await doc.background('generateSummary', { format: 'md' })
|
|
29
|
+
.delay('5m')
|
|
30
|
+
.priority('high')
|
|
31
|
+
.retries(5)
|
|
32
|
+
.queue('analysis')
|
|
33
|
+
.timeout(600000)
|
|
34
|
+
.enqueue();
|
|
35
|
+
|
|
36
|
+
// Wait for result (polling-based)
|
|
37
|
+
const result = await handle2.wait({ timeout: 60000, pollInterval: 100 });
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Run a TaskRunner to process jobs
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { TaskRunner } from '@happyvertical/smrt-jobs';
|
|
44
|
+
|
|
45
|
+
const runner = new TaskRunner({
|
|
46
|
+
concurrency: 5,
|
|
47
|
+
pollInterval: 1000,
|
|
48
|
+
queues: ['default', 'analysis'],
|
|
49
|
+
});
|
|
50
|
+
await runner.initialize(db);
|
|
51
|
+
await runner.start();
|
|
52
|
+
|
|
53
|
+
// Listen for events
|
|
54
|
+
runner.on('job:completed', (job, result) => { /* ... */ });
|
|
55
|
+
runner.on('job:failed', (job, error) => { /* ... */ });
|
|
56
|
+
|
|
57
|
+
// Graceful shutdown
|
|
58
|
+
process.on('SIGTERM', () => runner.stop());
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Heartbeat-safe job execution
|
|
62
|
+
|
|
63
|
+
`TaskRunner` keeps jobs alive with a heartbeat timer. If your job blocks the
|
|
64
|
+
Node.js event loop for longer than the effective stale-job threshold, the runner
|
|
65
|
+
will recover that work as stale and mark it failed.
|
|
66
|
+
|
|
67
|
+
Common causes:
|
|
68
|
+
|
|
69
|
+
- `execSync`, `spawnSync`, or other synchronous subprocess APIs
|
|
70
|
+
- long CPU-bound loops in the job method itself
|
|
71
|
+
- large synchronous filesystem work
|
|
72
|
+
|
|
73
|
+
Prefer async subprocess APIs (`spawn`, `execFile`, streamed stdio) for long
|
|
74
|
+
exports/builds/uploads, or move CPU-heavy work into a separate process or
|
|
75
|
+
worker thread. If a job is intentionally long-running, tune
|
|
76
|
+
`heartbeatInterval` and `staleJobThresholdMs` together so the stale threshold
|
|
77
|
+
comfortably exceeds the longest gap between heartbeats.
|
|
78
|
+
|
|
79
|
+
`ScheduleRunner` uses the same stale-heartbeat recovery rules when reconciling
|
|
80
|
+
scheduled jobs.
|
|
81
|
+
|
|
82
|
+
### Schedule recurring jobs with ScheduleRunner
|
|
83
|
+
|
|
84
|
+
The `ScheduleRunner` polls the `_smrt_agent_schedules` table for due cron entries and creates `SmrtJob` records for the `TaskRunner` to execute. Wire them together via events:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import { ScheduleRunner } from '@happyvertical/smrt-jobs';
|
|
88
|
+
|
|
89
|
+
const scheduleRunner = new ScheduleRunner({ pollInterval: 30000 });
|
|
90
|
+
await scheduleRunner.initialize(db);
|
|
91
|
+
await scheduleRunner.start();
|
|
92
|
+
|
|
93
|
+
// Connect TaskRunner events to update schedule state
|
|
94
|
+
taskRunner.on('job:completed', (job) => {
|
|
95
|
+
const scheduleId = job.args?._scheduleId;
|
|
96
|
+
if (scheduleId) scheduleRunner.handleJobCompletion(scheduleId, true);
|
|
97
|
+
});
|
|
98
|
+
taskRunner.on('job:failed', (job, error) => {
|
|
99
|
+
const scheduleId = job.args?._scheduleId;
|
|
100
|
+
if (scheduleId) scheduleRunner.handleJobCompletion(scheduleId, false, error.message);
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### System Tables
|
|
105
|
+
|
|
106
|
+
| Table | Purpose |
|
|
107
|
+
|-------|---------|
|
|
108
|
+
| `_smrt_jobs` | Persistent job queue (SmrtJob records) |
|
|
109
|
+
| `_smrt_agent_schedules` | Cron schedule entries polled by ScheduleRunner |
|
|
110
|
+
|
|
111
|
+
## API
|
|
112
|
+
|
|
113
|
+
### Classes
|
|
114
|
+
|
|
115
|
+
| Export | Description |
|
|
116
|
+
|--------|------------|
|
|
117
|
+
| `SmrtJob` | Persistent job record stored in `_smrt_jobs` |
|
|
118
|
+
| `SmrtJobCollection` | Collection with `claimReady()`, `listReady()`, `listByStatus()`, `stats()`, `cleanup()` |
|
|
119
|
+
| `JobBuilder` | Fluent API: `.delay()`, `.priority()`, `.retries()`, `.queue()`, `.timeout()`, `.enqueue()` |
|
|
120
|
+
| `JobHandle` | Track, wait, cancel, or retry an enqueued job |
|
|
121
|
+
| `JobContextLogger` | Logger that auto-injects job context (jobId, attempt, queue) |
|
|
122
|
+
| `TaskRunner` | Polling-based execution engine with concurrency control and heartbeats |
|
|
123
|
+
| `ScheduleRunner` | Polls for due cron schedules and creates SmrtJob entries |
|
|
124
|
+
|
|
125
|
+
`TaskRunner` uses `SmrtJobCollection.claimReady()` so multiple workers can poll
|
|
126
|
+
the same queue without duplicate-claiming a pending row.
|
|
127
|
+
|
|
128
|
+
### Functions
|
|
129
|
+
|
|
130
|
+
| Export | Description |
|
|
131
|
+
|--------|------------|
|
|
132
|
+
| `createTaskRunner(config?)` | Factory for creating a configured TaskRunner |
|
|
133
|
+
| `createScheduleRunner(config?)` | Factory for creating a configured ScheduleRunner |
|
|
134
|
+
| `withBackgroundJobs(Class)` | Mixin that adds `.bg()` and `.background()` to any SmrtObject class |
|
|
135
|
+
| `parseDelay(delay)` | Parse human-readable delay strings (`'5m'`, `'1h'`, `'30s'`) to milliseconds |
|
|
136
|
+
| `priorityToNumber(priority)` | Convert priority label (`'critical'`/`'high'`/`'normal'`/`'low'`) to number |
|
|
137
|
+
|
|
138
|
+
### Key Types
|
|
139
|
+
|
|
140
|
+
`Priority`, `JobStatus`, `JobResult`, `WaitOptions`, `BgOptions`, `BackgroundCapable`, `TaskRunnerConfig`, `TaskRunnerEvents`, `ScheduleRunnerConfig`, `ScheduleRunnerEvents`, `ScheduleInfo`, `JobContext`, `TimeoutBehavior`, `SmrtJobData`, `ListReadyOptions`
|
|
141
|
+
|
|
142
|
+
## Dependencies
|
|
143
|
+
|
|
144
|
+
- `@happyvertical/smrt-core` -- ORM and code generation
|
|
145
|
+
- `@happyvertical/smrt-config` -- configuration loading
|
|
146
|
+
- `@happyvertical/smrt-types` -- shared type definitions
|
|
147
|
+
- `@happyvertical/jobs` -- retry strategies
|
|
148
|
+
- `@happyvertical/sql` -- database interface
|
|
149
|
+
- `@happyvertical/logger` -- structured logging
|
|
150
|
+
- `@happyvertical/utils` -- ID generation utilities
|
|
151
|
+
- Peer (optional): `@happyvertical/smrt-svelte`, `svelte`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"__smrt-register__.d.ts","sourceRoot":"","sources":["../src/__smrt-register__.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in policy controls for background job creation and dispatch.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* These guards harden the background-jobs surface flagged by the S5 audit
|
|
6
|
+
* (#1402):
|
|
7
|
+
*
|
|
8
|
+
* - {@link MAX_JOB_RETRIES} caps the retry count a caller can request so a
|
|
9
|
+
* misconfigured `.retries(n)` cannot pin a worker on a poison job forever.
|
|
10
|
+
* - {@link assertWithinTenantCreationCap} bounds how many jobs a single tenant
|
|
11
|
+
* may hold in the queue at once, so one tenant cannot exhaust the shared
|
|
12
|
+
* worker pool (a cross-tenant denial of service).
|
|
13
|
+
* - {@link isBackgroundEligibleMethod} / {@link backgroundEligible} provide an
|
|
14
|
+
* opt-in allowlist of methods that may be invoked by the runner. The runner's
|
|
15
|
+
* dispatch is already bounded to existing prototype methods (no eval / dynamic
|
|
16
|
+
* import), but a class can further restrict which of its methods are reachable
|
|
17
|
+
* from a persisted job row. This same marker is intended to be consumed by the
|
|
18
|
+
* agents package, which dispatches methods through an equivalent path.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Hard ceiling on retry attempts a caller may request via `.retries(n)` /
|
|
22
|
+
* `bg(..., { retries })`. Requests above this are clamped (not rejected) so
|
|
23
|
+
* existing callers keep working while the worst case stays bounded.
|
|
24
|
+
*/
|
|
25
|
+
export declare const MAX_JOB_RETRIES = 25;
|
|
26
|
+
/**
|
|
27
|
+
* Default maximum number of non-terminal (pending/running) jobs a single
|
|
28
|
+
* tenant may hold in the queue at once. Configurable per call; `0` / negative
|
|
29
|
+
* disables the cap.
|
|
30
|
+
*/
|
|
31
|
+
export declare const DEFAULT_TENANT_JOB_CAP = 10000;
|
|
32
|
+
/**
|
|
33
|
+
* Clamp a requested retry count to {@link MAX_JOB_RETRIES}.
|
|
34
|
+
*
|
|
35
|
+
* @param requested - The retry count supplied by the caller.
|
|
36
|
+
* @returns A non-negative integer no greater than {@link MAX_JOB_RETRIES}.
|
|
37
|
+
*/
|
|
38
|
+
export declare function clampRetries(requested: number): number;
|
|
39
|
+
/**
|
|
40
|
+
* Error thrown when a tenant exceeds its allowed in-flight job count.
|
|
41
|
+
*/
|
|
42
|
+
export declare class TenantJobCapExceededError extends Error {
|
|
43
|
+
readonly tenantId: string;
|
|
44
|
+
readonly cap: number;
|
|
45
|
+
readonly current: number;
|
|
46
|
+
constructor(tenantId: string, cap: number, current: number);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Throw {@link TenantJobCapExceededError} when a tenant is at or above its cap.
|
|
50
|
+
*
|
|
51
|
+
* @param tenantId - Tenant the new job would belong to (`null` = global; not
|
|
52
|
+
* subject to the per-tenant cap).
|
|
53
|
+
* @param current - Current count of non-terminal jobs for the tenant.
|
|
54
|
+
* @param cap - Maximum allowed; `<= 0` disables the check.
|
|
55
|
+
*/
|
|
56
|
+
export declare function assertWithinTenantCreationCap(tenantId: string | null | undefined, current: number, cap: number): void;
|
|
57
|
+
/**
|
|
58
|
+
* Class shape that opts into a background-method allowlist by declaring a
|
|
59
|
+
* static set/array of method names.
|
|
60
|
+
*/
|
|
61
|
+
export interface BackgroundEligibleClass {
|
|
62
|
+
/**
|
|
63
|
+
* Method names that may be invoked by the job/agent runner. When present
|
|
64
|
+
* (even if empty), it is treated as an exhaustive allowlist. When absent,
|
|
65
|
+
* the runner falls back to its default behaviour (any existing method).
|
|
66
|
+
*/
|
|
67
|
+
backgroundEligibleMethods?: ReadonlyArray<string> | ReadonlySet<string>;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Add method names to a class's background-eligible allowlist.
|
|
71
|
+
*
|
|
72
|
+
* Installs/extends a static `backgroundEligibleMethods` set on the constructor.
|
|
73
|
+
* Once any method is marked, the runner refuses to dispatch a job whose
|
|
74
|
+
* `method` is not in the set — turning the dispatch surface from "any prototype
|
|
75
|
+
* method" into an explicit contract. Use this when applying the
|
|
76
|
+
* {@link backgroundEligible} decorator is inconvenient (or in non-decorator
|
|
77
|
+
* code, including the agents package).
|
|
78
|
+
*
|
|
79
|
+
* @param ctor - The class constructor to annotate.
|
|
80
|
+
* @param methods - Method names to allow.
|
|
81
|
+
*/
|
|
82
|
+
export declare function markBackgroundEligible(ctor: object, ...methods: string[]): void;
|
|
83
|
+
/**
|
|
84
|
+
* Decorator: mark a method as background-eligible.
|
|
85
|
+
*
|
|
86
|
+
* This is a legacy (`experimentalDecorators`) method decorator — the mode the
|
|
87
|
+
* SMRT monorepo compiles with. Applying it (one or more times) builds up the
|
|
88
|
+
* static `backgroundEligibleMethods` allowlist on the owning class. Once any
|
|
89
|
+
* method is marked, the runner will refuse to dispatch a job whose `method` is
|
|
90
|
+
* not in the set.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* class Report extends SmrtObject {
|
|
95
|
+
* \@backgroundEligible()
|
|
96
|
+
* async regenerate() {} // reachable from a job
|
|
97
|
+
*
|
|
98
|
+
* async deleteEverything() {} // NOT reachable from a job
|
|
99
|
+
* }
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export declare function backgroundEligible(): (target: object, propertyKey: string | symbol, descriptor?: PropertyDescriptor) => PropertyDescriptor | undefined;
|
|
103
|
+
/**
|
|
104
|
+
* Resolve the declared allowlist for a class, if any.
|
|
105
|
+
*
|
|
106
|
+
* @param ctor - The target object's constructor.
|
|
107
|
+
* @returns A `Set` of allowed method names, or `null` when the class did not
|
|
108
|
+
* opt in (runner should fall back to default behaviour).
|
|
109
|
+
*/
|
|
110
|
+
export declare function getBackgroundEligibleMethods(ctor: unknown): ReadonlySet<string> | null;
|
|
111
|
+
/**
|
|
112
|
+
* Whether a method may be invoked by the runner for a given target class.
|
|
113
|
+
*
|
|
114
|
+
* @param ctor - Constructor of the resolved target class.
|
|
115
|
+
* @param method - Method name from the persisted job row.
|
|
116
|
+
* @returns `true` when the class declared no allowlist (default) or when the
|
|
117
|
+
* method is on the allowlist; `false` when an allowlist exists and excludes
|
|
118
|
+
* the method.
|
|
119
|
+
*/
|
|
120
|
+
export declare function isBackgroundEligibleMethod(ctor: unknown, method: string): boolean;
|
|
121
|
+
//# sourceMappingURL=background-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"background-policy.d.ts","sourceRoot":"","sources":["../src/background-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;GAIG;AACH,eAAO,MAAM,eAAe,KAAK,CAAC;AAElC;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,QAAS,CAAC;AAE7C;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAQtD;AAED;;GAEG;AACH,qBAAa,yBAA0B,SAAQ,KAAK;aAEhC,QAAQ,EAAE,MAAM;aAChB,GAAG,EAAE,MAAM;aACX,OAAO,EAAE,MAAM;gBAFf,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM;CAQlC;AAED;;;;;;;GAOG;AACH,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACnC,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,GACV,IAAI,CAKN;AAED;;;GAGG;AACH,MAAM,WAAW,uBAAuB;IACtC;;;;OAIG;IACH,yBAAyB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;CACzE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,MAAM,EACZ,GAAG,OAAO,EAAE,MAAM,EAAE,GACnB,IAAI,CASN;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,KAE9B,QAAQ,MAAM,EACd,aAAa,MAAM,GAAG,MAAM,EAC5B,aAAa,kBAAkB,KAC9B,kBAAkB,GAAG,SAAS,CAOlC;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,OAAO,GACZ,WAAW,CAAC,MAAM,CAAC,GAAG,IAAI,CAK5B;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,OAAO,EACb,MAAM,EAAE,MAAM,GACb,OAAO,CAIT"}
|