c8y-nitro 0.7.2 → 0.8.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/dist/cli/index.mjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{package-BHOzdhHp.mjs → package-CNWz76PV.mjs} +1 -1
- package/dist/utils.d.mts +229 -75
- package/dist/utils.mjs +153 -155
- package/package.json +2 -2
package/dist/cli/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as name, r as version, t as description } from "../package-
|
|
1
|
+
import { n as name, r as version, t as description } from "../package-CNWz76PV.mjs";
|
|
2
2
|
import { defineCommand, runMain } from "citty";
|
|
3
3
|
//#region src/cli/index.ts
|
|
4
4
|
runMain(defineCommand({
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { _ as GENERATED_READINESS_ROUTE, g as GENERATED_LIVENESS_ROUTE, h as GENERATED_INVALIDATE_TENANT_OPTIONS_ROUTE, m as createC8yManifestFromNitro, p as createC8yManifest } from "./c8y-api-DscUs-Kd.mjs";
|
|
2
2
|
import { t as autoBootstrap } from "./autoBootstrap-Drk7ltXK.mjs";
|
|
3
|
-
import { n as name } from "./package-
|
|
3
|
+
import { n as name } from "./package-CNWz76PV.mjs";
|
|
4
4
|
import { basename, dirname, join, relative } from "node:path";
|
|
5
5
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
6
6
|
import { x } from "tinyexec";
|
package/dist/utils.d.mts
CHANGED
|
@@ -502,110 +502,264 @@ declare function useLogger<T extends object = Record<string, unknown>>(eventOrRe
|
|
|
502
502
|
*/
|
|
503
503
|
declare const createError: typeof createError$1;
|
|
504
504
|
//#endregion
|
|
505
|
-
//#region src/utils/
|
|
506
|
-
type ScheduledTaskPayload = TaskPayload;
|
|
507
|
-
type ScheduledTaskContext = TaskContext;
|
|
505
|
+
//#region src/utils/tasks.d.ts
|
|
508
506
|
/**
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
507
|
+
* Concurrency policy for a single job.
|
|
508
|
+
* - `'single'`: at most one run of the job is in flight at a time. A tick that
|
|
509
|
+
* fires while the previous run is still going is skipped, and a manual
|
|
510
|
+
* {@link TaskRegistry.triggerJob} while a run is active joins the in-flight run.
|
|
511
|
+
* - `'parallel'`: every tick (or trigger) starts a fresh run, even if a previous
|
|
512
|
+
* run has not finished. Runs of the same job may overlap.
|
|
512
513
|
*/
|
|
513
|
-
type
|
|
514
|
+
type JobConcurrency = 'single' | 'parallel';
|
|
515
|
+
type TaskRegistryPayload = TaskPayload;
|
|
516
|
+
type TaskRegistryContext = TaskContext;
|
|
514
517
|
/**
|
|
515
|
-
*
|
|
516
|
-
* `payload` and `context` are forwarded to the task handler's `run()` method.
|
|
518
|
+
* The event passed to a task handler on every execution.
|
|
517
519
|
*/
|
|
518
|
-
interface
|
|
520
|
+
interface TaskEvent<TPayload = TaskRegistryPayload> {
|
|
519
521
|
/**
|
|
520
|
-
*
|
|
522
|
+
* Name of the task (function) being executed.
|
|
521
523
|
*/
|
|
522
|
-
|
|
524
|
+
task: string;
|
|
525
|
+
/**
|
|
526
|
+
* Name of the job that triggered this run, when the run originates from a scheduled or triggered job.
|
|
527
|
+
*/
|
|
528
|
+
job?: string;
|
|
529
|
+
/**
|
|
530
|
+
* Data bound when the job was scheduled (or passed to `run()`/`triggerJob()`).
|
|
531
|
+
*/
|
|
532
|
+
payload: TPayload;
|
|
533
|
+
/**
|
|
534
|
+
* Arbitrary context bound alongside the payload.
|
|
535
|
+
*/
|
|
536
|
+
context: TaskRegistryContext;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* A task handler function registered via {@link TaskRegistry.createTask}.
|
|
540
|
+
*/
|
|
541
|
+
type TaskHandler<TPayload = TaskRegistryPayload, TResult = unknown> = (event: TaskEvent<TPayload>) => TResult | Promise<TResult>;
|
|
542
|
+
/**
|
|
543
|
+
* When a job should run.
|
|
544
|
+
* - `{ cron }`: recurring on a cron expression. 5- or 6-field (with seconds) is
|
|
545
|
+
* supported. Evaluated in UTC by default, or in the job's `timezone` if set.
|
|
546
|
+
* - `{ in }`: one-shot, `in` seconds from now.
|
|
547
|
+
* - `{ at }`: one-shot, at an exact instant given as a `Date` or ISO 8601 string,
|
|
548
|
+
* taken literally (never re-projected to another timezone). Times in the past
|
|
549
|
+
* fire almost immediately.
|
|
550
|
+
*/
|
|
551
|
+
type JobSchedule = {
|
|
552
|
+
cron: string;
|
|
553
|
+
} | {
|
|
554
|
+
in: number;
|
|
555
|
+
} | {
|
|
556
|
+
at: Date | string;
|
|
557
|
+
};
|
|
558
|
+
/**
|
|
559
|
+
* Options for scheduling a job via {@link TaskRegistry.scheduleJob}.
|
|
560
|
+
*/
|
|
561
|
+
interface ScheduleJobOptions<TTask extends string = string, TPayload = TaskRegistryPayload, TSchedule extends JobSchedule = JobSchedule> {
|
|
562
|
+
/**
|
|
563
|
+
* Unique, caller-chosen job name. This is the handle used by `cancelJob()`, `getJob()` and `triggerJob()`.
|
|
564
|
+
*/
|
|
565
|
+
name: string;
|
|
566
|
+
/**
|
|
567
|
+
* Name of a task registered via `createTask()` that this job runs.
|
|
568
|
+
*/
|
|
569
|
+
task: TTask;
|
|
570
|
+
/**
|
|
571
|
+
* When the job should run.
|
|
572
|
+
*/
|
|
573
|
+
schedule: TSchedule;
|
|
574
|
+
/**
|
|
575
|
+
* Data forwarded to the handler's `event.payload`.\
|
|
576
|
+
* Prefer stable identifiers (e.g. a config id) over live object references so the
|
|
577
|
+
* handler can resolve current values at run time and the schedule stays serializable.
|
|
578
|
+
*/
|
|
579
|
+
payload?: TPayload;
|
|
523
580
|
/**
|
|
524
|
-
*
|
|
581
|
+
* Data forwarded to the handler's `event.context`.
|
|
525
582
|
*/
|
|
526
|
-
context?:
|
|
583
|
+
context?: TaskRegistryContext;
|
|
527
584
|
/**
|
|
528
|
-
*
|
|
585
|
+
* Concurrency policy for this job. Defaults to `'single'`.
|
|
529
586
|
*/
|
|
530
|
-
|
|
587
|
+
concurrency?: JobConcurrency;
|
|
588
|
+
/**
|
|
589
|
+
* Recurring only: the IANA timezone the cron expression is evaluated in
|
|
590
|
+
* (e.g. `'Europe/Berlin'`). Defaults to `'UTC'` so the schedule is deterministic
|
|
591
|
+
* regardless of the container's local time. croner performs the alignment — no
|
|
592
|
+
* conversion is done by `c8y-nitro`. Ignored for one-shot schedules.
|
|
593
|
+
*/
|
|
594
|
+
timezone?: string;
|
|
595
|
+
/**
|
|
596
|
+
* Recurring only: also run once immediately on registration, in addition to the cron. Defaults to `false`.
|
|
597
|
+
*/
|
|
598
|
+
immediate?: boolean;
|
|
599
|
+
/**
|
|
600
|
+
* Recurring only: stop after this many scheduled runs.
|
|
601
|
+
*/
|
|
602
|
+
maxRuns?: number;
|
|
603
|
+
/**
|
|
604
|
+
* Overwrite an existing job with the same name instead of throwing. Defaults to `false`.
|
|
605
|
+
*/
|
|
606
|
+
replace?: boolean;
|
|
531
607
|
}
|
|
532
608
|
/**
|
|
533
|
-
*
|
|
534
|
-
* Returned by `scheduleTask()` and by `listScheduledTasks()`.
|
|
609
|
+
* Fields common to every job, regardless of kind.
|
|
535
610
|
*/
|
|
536
|
-
interface
|
|
611
|
+
interface BaseJobInfo {
|
|
537
612
|
/**
|
|
538
|
-
*
|
|
613
|
+
* The job's unique name.
|
|
539
614
|
*/
|
|
540
|
-
|
|
615
|
+
name: string;
|
|
541
616
|
/**
|
|
542
|
-
*
|
|
617
|
+
* The task (function) the job runs.
|
|
543
618
|
*/
|
|
544
619
|
task: string;
|
|
545
620
|
/**
|
|
546
|
-
*
|
|
621
|
+
* The job's concurrency policy.
|
|
622
|
+
*/
|
|
623
|
+
concurrency: JobConcurrency;
|
|
624
|
+
/**
|
|
625
|
+
* Next scheduled execution as an ISO datetime string, or `null` if nothing is pending.
|
|
626
|
+
*/
|
|
627
|
+
nextRun: string | null;
|
|
628
|
+
/**
|
|
629
|
+
* Whether a run of this job is currently in flight.
|
|
547
630
|
*/
|
|
548
|
-
|
|
631
|
+
running: boolean;
|
|
549
632
|
}
|
|
550
633
|
/**
|
|
551
|
-
*
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
*
|
|
562
|
-
* payload: { messageId: 'abc123' },
|
|
563
|
-
* schedule: 30,
|
|
564
|
-
* })
|
|
565
|
-
*
|
|
566
|
-
* @example
|
|
567
|
-
* // Run a task using a human-readable duration:
|
|
568
|
-
* await scheduleTask('reports:generate', {
|
|
569
|
-
* payload: { reportId: 'report-1' },
|
|
570
|
-
* schedule: '1 hour',
|
|
571
|
-
* })
|
|
572
|
-
*
|
|
573
|
-
* @example
|
|
574
|
-
* // Run a task at an exact time:
|
|
575
|
-
* await scheduleTask('cleanup:tenant', {
|
|
576
|
-
* payload: { tenant: 't12345' },
|
|
577
|
-
* schedule: new Date('2026-05-01T12:00:00Z'),
|
|
578
|
-
* })
|
|
634
|
+
* Information about a one-shot job (scheduled with `{ in }` or `{ at }`).
|
|
635
|
+
*/
|
|
636
|
+
interface OnceJobInfo extends BaseJobInfo {
|
|
637
|
+
kind: 'once';
|
|
638
|
+
/**
|
|
639
|
+
* Always `null` for one-shot jobs.
|
|
640
|
+
*/
|
|
641
|
+
cron: null;
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Information about a recurring job (scheduled with `{ cron }`).
|
|
579
645
|
*/
|
|
580
|
-
|
|
646
|
+
interface RecurringJobInfo extends BaseJobInfo {
|
|
647
|
+
kind: 'recurring';
|
|
648
|
+
/**
|
|
649
|
+
* The cron expression the job runs on.
|
|
650
|
+
*/
|
|
651
|
+
cron: string;
|
|
652
|
+
/**
|
|
653
|
+
* The IANA timezone the cron expression is evaluated in.
|
|
654
|
+
*/
|
|
655
|
+
timezone: string;
|
|
656
|
+
/**
|
|
657
|
+
* A few upcoming executions as ISO datetime strings.
|
|
658
|
+
*/
|
|
659
|
+
nextRuns: string[];
|
|
660
|
+
}
|
|
581
661
|
/**
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
* @returns Object mapping scheduled task IDs to their public task information
|
|
586
|
-
*
|
|
587
|
-
* @example
|
|
588
|
-
* const tasks = await listScheduledTasks()
|
|
589
|
-
* for (const [id, task] of Object.entries(tasks)) {
|
|
590
|
-
* console.log(id, task.task, task.runAt)
|
|
591
|
-
* }
|
|
662
|
+
* Public information about a registered job. A discriminated union on `kind` —
|
|
663
|
+
* returned by `scheduleJob()`, `getJob()` and `listJobs()`.
|
|
592
664
|
*/
|
|
593
|
-
|
|
665
|
+
type JobInfo = OnceJobInfo | RecurringJobInfo;
|
|
594
666
|
/**
|
|
595
|
-
*
|
|
596
|
-
|
|
667
|
+
* Maps a {@link JobSchedule} to the {@link JobInfo} variant it produces.
|
|
668
|
+
*/
|
|
669
|
+
type JobInfoFor<TSchedule extends JobSchedule> = TSchedule extends {
|
|
670
|
+
cron: string;
|
|
671
|
+
} ? RecurringJobInfo : OnceJobInfo;
|
|
672
|
+
/**
|
|
673
|
+
* Describes the payload and result type of a single registered task.
|
|
674
|
+
*/
|
|
675
|
+
interface TaskShape<TPayload = TaskRegistryPayload, TResult = unknown> {
|
|
676
|
+
payload: TPayload;
|
|
677
|
+
result: TResult;
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* A map of task name to its {@link TaskShape}, accumulated by the builder.
|
|
681
|
+
*/
|
|
682
|
+
type TaskMap = Record<string, TaskShape>;
|
|
683
|
+
/**
|
|
684
|
+
* A type-charged registry of tasks (functions) and the jobs that schedule them.
|
|
597
685
|
*
|
|
598
|
-
*
|
|
599
|
-
*
|
|
686
|
+
* Each `createTask()` call widens the registry's type with the new task name, so
|
|
687
|
+
* `run()`, `scheduleJob()` and `triggerJob()` autocomplete known task names and
|
|
688
|
+
* reject typos at compile time.
|
|
689
|
+
*/
|
|
690
|
+
interface TaskRegistry<TTasks extends TaskMap = {}> {
|
|
691
|
+
/**
|
|
692
|
+
* Registers a task (function) under a compile-time-known name.\
|
|
693
|
+
* Passing a name that is already registered is a type error (and throws at runtime).
|
|
694
|
+
*
|
|
695
|
+
* @param name - Unique task name
|
|
696
|
+
* @param handler - The function to run. Annotate its event to type the payload,
|
|
697
|
+
* e.g. `(e: TaskEvent<{ configId: string }>) => ...`.
|
|
698
|
+
*/
|
|
699
|
+
createTask: <TName extends string, TPayload extends TaskRegistryPayload = TaskRegistryPayload, TResult = unknown>(name: TName extends keyof TTasks ? never : TName, handler: TaskHandler<TPayload, TResult>) => TaskRegistry<TTasks & Record<TName, TaskShape<TPayload, TResult>>>;
|
|
700
|
+
/**
|
|
701
|
+
* Runs a task immediately and ad-hoc, without registering a job. Always runs
|
|
702
|
+
* fresh (no deduplication). Use a job with `'single'` concurrency when you need
|
|
703
|
+
* overlap protection.
|
|
704
|
+
*/
|
|
705
|
+
run: <TName extends keyof TTasks & string>(task: TName, options?: {
|
|
706
|
+
payload?: TTasks[TName]['payload'];
|
|
707
|
+
context?: TaskRegistryContext;
|
|
708
|
+
}) => Promise<TTasks[TName]['result']>;
|
|
709
|
+
/**
|
|
710
|
+
* Schedules a named job that runs a task once or repeatedly. The return type is
|
|
711
|
+
* inferred from the schedule: `{ cron }` yields a {@link RecurringJobInfo},
|
|
712
|
+
* `{ in }`/`{ at }` yield a {@link OnceJobInfo}.
|
|
713
|
+
*/
|
|
714
|
+
scheduleJob: <TName extends keyof TTasks & string, TSchedule extends JobSchedule>(options: ScheduleJobOptions<TName, TTasks[TName]['payload'], TSchedule>) => JobInfoFor<TSchedule>;
|
|
715
|
+
/**
|
|
716
|
+
* Runs an already-registered job immediately, respecting its concurrency policy.
|
|
717
|
+
* For a `'single'` job with a run in flight this returns the in-flight promise.
|
|
718
|
+
*/
|
|
719
|
+
triggerJob: <TResult = unknown>(name: string, options?: {
|
|
720
|
+
payload?: TaskRegistryPayload;
|
|
721
|
+
context?: TaskRegistryContext;
|
|
722
|
+
}) => Promise<TResult>;
|
|
723
|
+
/**
|
|
724
|
+
* Cancels a scheduled job by name.
|
|
725
|
+
* @returns `true` when a job was found and cancelled, otherwise `false`.
|
|
726
|
+
*/
|
|
727
|
+
cancelJob: (name: string) => boolean;
|
|
728
|
+
/**
|
|
729
|
+
* Lists all currently registered jobs.
|
|
730
|
+
*/
|
|
731
|
+
listJobs: () => JobInfo[];
|
|
732
|
+
/**
|
|
733
|
+
* Returns information about a single job, or `undefined` if it does not exist.
|
|
734
|
+
*/
|
|
735
|
+
getJob: (name: string) => JobInfo | undefined;
|
|
736
|
+
/**
|
|
737
|
+
* Returns `true` when a task with the given name is registered.
|
|
738
|
+
*/
|
|
739
|
+
hasTask: (name: string) => boolean;
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Creates a new, empty task registry.
|
|
600
743
|
*
|
|
601
744
|
* @example
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
* })
|
|
745
|
+
* import { c8yTasks } from 'c8y-nitro/utils'
|
|
746
|
+
*
|
|
747
|
+
* export const tasks = c8yTasks()
|
|
748
|
+
* .createTask('sync', async ({ payload }) => {
|
|
749
|
+
* const config = await loadConfig(payload.configId) // resolve live state here
|
|
750
|
+
* await sync(config)
|
|
751
|
+
* })
|
|
752
|
+
*
|
|
753
|
+
* // one per config, each an independent job
|
|
754
|
+
* tasks.scheduleJob({ name: 'sync-abc', task: 'sync', payload: { configId: 'abc' }, schedule: { cron: '*\/5 * * * *' } })
|
|
755
|
+
* tasks.scheduleJob({ name: 'sync-def', task: 'sync', payload: { configId: 'def' }, schedule: { cron: '*\/5 * * * *' }, concurrency: 'parallel' })
|
|
606
756
|
*
|
|
607
|
-
*
|
|
757
|
+
* @remarks
|
|
758
|
+
* The registry is a plain runtime singleton — export it from one module and import
|
|
759
|
+
* it wherever you schedule or cancel jobs. Recurring jobs only arm when the module
|
|
760
|
+
* that schedules them executes, so register them from a Nitro plugin (or another
|
|
761
|
+
* startup path) if they must run from boot.
|
|
608
762
|
*/
|
|
609
|
-
declare function
|
|
763
|
+
declare function c8yTasks(): TaskRegistry;
|
|
610
764
|
//#endregion
|
|
611
|
-
export { ForeignTenantOptionCategory, ForeignTenantOptionKeyInput,
|
|
765
|
+
export { BaseJobInfo, ForeignTenantOptionCategory, ForeignTenantOptionKeyInput, JobConcurrency, JobInfo, JobInfoFor, JobSchedule, OnceJobInfo, RecurringJobInfo, ScheduleJobOptions, SetEditableOptions, TaskEvent, TaskHandler, TaskMap, TaskRegistry, TaskRegistryContext, TaskRegistryPayload, TaskShape, TenantOption, TenantOptionCategory, TenantOptionKeyInput, c8yTasks, createError, createLogger, hasUserRequiredRole, isUserFromAllowedTenant, isUserFromDeployedTenant, useDeployedTenantClient, useDeployedTenantCredentials, useLogger, useSubscribedTenantClients, useSubscribedTenantCredentials, useTenantOption, useTenantOptions, useUser, useUserClient, useUserRoles, useUserTenantClient, useUserTenantCredentials };
|
package/dist/utils.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
|
-
import { createHash, randomBytes
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { useLogger as useLogger$1 } from "evlog/nitro/v3";
|
|
5
5
|
import { BasicAuth, Client, MicroserviceClientRequestAuth } from "@c8y/client";
|
|
@@ -9,8 +9,7 @@ import { useRuntimeConfig } from "nitro/runtime-config";
|
|
|
9
9
|
import { useNitroHooks } from "nitro/app";
|
|
10
10
|
import { defineHandler } from "nitro/h3";
|
|
11
11
|
import { c8yManifest } from "c8y-nitro/runtime";
|
|
12
|
-
import {
|
|
13
|
-
import { tasks } from "#nitro/virtual/tasks";
|
|
12
|
+
import { Cron } from "croner";
|
|
14
13
|
//#region src/utils/internal/common.ts
|
|
15
14
|
/**
|
|
16
15
|
* Converts undici Request headers to the format expected by MicroserviceClientRequestAuth.\
|
|
@@ -884,168 +883,167 @@ const useTenantOptions = Object.assign(useTenantOptionsBase, {
|
|
|
884
883
|
refreshAll: () => refreshFetchers(() => true)
|
|
885
884
|
});
|
|
886
885
|
//#endregion
|
|
887
|
-
//#region src/utils/
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
const scheduledTaskTimers = /* @__PURE__ */ new Map();
|
|
893
|
-
const schedulerReady = Promise.resolve();
|
|
894
|
-
let schedulerInterval;
|
|
895
|
-
function createScheduledTaskId() {
|
|
896
|
-
return randomUUID();
|
|
897
|
-
}
|
|
898
|
-
function resolveScheduleTime(schedule) {
|
|
899
|
-
if (schedule instanceof Date) {
|
|
900
|
-
const timestamp = schedule.getTime();
|
|
901
|
-
if (Number.isNaN(timestamp)) throw new TypeError("schedule date must be valid");
|
|
902
|
-
return timestamp;
|
|
903
|
-
}
|
|
904
|
-
if (typeof schedule === "number") {
|
|
905
|
-
if (!Number.isFinite(schedule) || schedule < 0) throw new TypeError("schedule number must be a non-negative number of seconds");
|
|
906
|
-
return Date.now() + schedule * 1e3;
|
|
886
|
+
//#region src/utils/tasks.ts
|
|
887
|
+
function resolveOnceDate(schedule) {
|
|
888
|
+
if ("in" in schedule) {
|
|
889
|
+
if (!Number.isFinite(schedule.in) || schedule.in < 0) throw new TypeError("schedule.in must be a non-negative number of seconds");
|
|
890
|
+
return new Date(Date.now() + schedule.in * 1e3);
|
|
907
891
|
}
|
|
908
|
-
const
|
|
909
|
-
if (
|
|
910
|
-
return
|
|
911
|
-
}
|
|
912
|
-
async function executeScheduledTask(id) {
|
|
913
|
-
const record = scheduledTasks.get(id);
|
|
914
|
-
if (!record) return;
|
|
915
|
-
if (record.timeoutId !== void 0) scheduledTaskTimers.delete(record.timeoutId);
|
|
916
|
-
scheduledTasks.delete(id);
|
|
917
|
-
stopSchedulerIntervalIfIdle();
|
|
918
|
-
const taskDef = tasks[record.taskName];
|
|
919
|
-
if (!taskDef) throw new Error(`Task "${record.taskName}" is not available!`);
|
|
920
|
-
if (!taskDef.resolve) throw new Error(`Task "${record.taskName}" is not implemented!`);
|
|
921
|
-
await (await taskDef.resolve()).run({
|
|
922
|
-
name: record.taskName,
|
|
923
|
-
payload: record.payload,
|
|
924
|
-
context: record.context
|
|
925
|
-
});
|
|
926
|
-
}
|
|
927
|
-
function armScheduledTaskTimeout(id) {
|
|
928
|
-
const record = scheduledTasks.get(id);
|
|
929
|
-
if (!record || record.timeoutId !== void 0) return;
|
|
930
|
-
const remainingMs = Math.max(record.runAt - Date.now(), 0);
|
|
931
|
-
if (remainingMs > SCHEDULE_LOOKAHEAD_MS) return;
|
|
932
|
-
const timeout = setTimeout(() => {
|
|
933
|
-
executeScheduledTask(id);
|
|
934
|
-
}, Math.min(remainingMs, MAX_TIMEOUT_MS));
|
|
935
|
-
record.timeoutId = Number(timeout);
|
|
936
|
-
scheduledTaskTimers.set(record.timeoutId, timeout);
|
|
937
|
-
}
|
|
938
|
-
function runSchedulerTick() {
|
|
939
|
-
for (const record of scheduledTasks.values()) armScheduledTaskTimeout(record.id);
|
|
940
|
-
}
|
|
941
|
-
function ensureSchedulerInterval() {
|
|
942
|
-
if (schedulerInterval) return;
|
|
943
|
-
schedulerInterval = setInterval(runSchedulerTick, SCHEDULER_TICK_MS);
|
|
944
|
-
}
|
|
945
|
-
function stopSchedulerIntervalIfIdle() {
|
|
946
|
-
if (!schedulerInterval || scheduledTasks.size > 0) return;
|
|
947
|
-
clearInterval(schedulerInterval);
|
|
948
|
-
schedulerInterval = void 0;
|
|
949
|
-
}
|
|
950
|
-
/**
|
|
951
|
-
* Schedules a Nitro task to run once in the future.\
|
|
952
|
-
* Resolves and calls the task handler directly from Nitro's virtual task registry\
|
|
953
|
-
* Numbers are treated as seconds, strings are parsed as human-readable durations, and dates are used as exact run times.
|
|
954
|
-
*
|
|
955
|
-
* @param taskName - The Nitro task name to run (from `tasks/*.ts`)
|
|
956
|
-
* @param options - Task payload, context, and the schedule time
|
|
957
|
-
* @returns Information about the scheduled task
|
|
958
|
-
*
|
|
959
|
-
* @example
|
|
960
|
-
* // Run a task in 30 seconds:
|
|
961
|
-
* const scheduled = await scheduleTask('emails:send', {
|
|
962
|
-
* payload: { messageId: 'abc123' },
|
|
963
|
-
* schedule: 30,
|
|
964
|
-
* })
|
|
965
|
-
*
|
|
966
|
-
* @example
|
|
967
|
-
* // Run a task using a human-readable duration:
|
|
968
|
-
* await scheduleTask('reports:generate', {
|
|
969
|
-
* payload: { reportId: 'report-1' },
|
|
970
|
-
* schedule: '1 hour',
|
|
971
|
-
* })
|
|
972
|
-
*
|
|
973
|
-
* @example
|
|
974
|
-
* // Run a task at an exact time:
|
|
975
|
-
* await scheduleTask('cleanup:tenant', {
|
|
976
|
-
* payload: { tenant: 't12345' },
|
|
977
|
-
* schedule: new Date('2026-05-01T12:00:00Z'),
|
|
978
|
-
* })
|
|
979
|
-
*/
|
|
980
|
-
async function scheduleTask(taskName, options) {
|
|
981
|
-
if (!import.meta._tasks) throw new Error("scheduleTask() requires tasks to be enabled. Set `experimental: { tasks: true }` in your nitro.config.ts.");
|
|
982
|
-
if (!taskName) throw new TypeError("taskName is required");
|
|
983
|
-
const runAt = resolveScheduleTime(options.schedule);
|
|
984
|
-
const record = {
|
|
985
|
-
id: createScheduledTaskId(),
|
|
986
|
-
taskName,
|
|
987
|
-
payload: options.payload ?? {},
|
|
988
|
-
context: options.context ?? {},
|
|
989
|
-
runAt
|
|
990
|
-
};
|
|
991
|
-
scheduledTasks.set(record.id, record);
|
|
992
|
-
armScheduledTaskTimeout(record.id);
|
|
993
|
-
ensureSchedulerInterval();
|
|
994
|
-
return {
|
|
995
|
-
id: record.id,
|
|
996
|
-
task: record.taskName,
|
|
997
|
-
runAt: new Date(record.runAt).toISOString()
|
|
998
|
-
};
|
|
892
|
+
const date = schedule.at instanceof Date ? schedule.at : new Date(schedule.at);
|
|
893
|
+
if (Number.isNaN(date.getTime())) throw new TypeError("schedule.at must be a valid Date or ISO 8601 string");
|
|
894
|
+
return date;
|
|
999
895
|
}
|
|
1000
896
|
/**
|
|
1001
|
-
*
|
|
1002
|
-
* The returned object is keyed by the scheduled task UUID for easy lookup and cancellation.
|
|
1003
|
-
*
|
|
1004
|
-
* @returns Object mapping scheduled task IDs to their public task information
|
|
897
|
+
* Creates a new, empty task registry.
|
|
1005
898
|
*
|
|
1006
899
|
* @example
|
|
1007
|
-
*
|
|
1008
|
-
* for (const [id, task] of Object.entries(tasks)) {
|
|
1009
|
-
* console.log(id, task.task, task.runAt)
|
|
1010
|
-
* }
|
|
1011
|
-
*/
|
|
1012
|
-
async function listScheduledTasks() {
|
|
1013
|
-
await schedulerReady;
|
|
1014
|
-
return Object.fromEntries([...scheduledTasks.values()].map((record) => [record.id, {
|
|
1015
|
-
id: record.id,
|
|
1016
|
-
task: record.taskName,
|
|
1017
|
-
runAt: new Date(record.runAt).toISOString()
|
|
1018
|
-
}]));
|
|
1019
|
-
}
|
|
1020
|
-
/**
|
|
1021
|
-
* Cancels a scheduled task before it starts running.\
|
|
1022
|
-
* Once the underlying Nitro task has started, it cannot be cancelled with this utility.
|
|
900
|
+
* import { c8yTasks } from 'c8y-nitro/utils'
|
|
1023
901
|
*
|
|
1024
|
-
*
|
|
1025
|
-
*
|
|
902
|
+
* export const tasks = c8yTasks()
|
|
903
|
+
* .createTask('sync', async ({ payload }) => {
|
|
904
|
+
* const config = await loadConfig(payload.configId) // resolve live state here
|
|
905
|
+
* await sync(config)
|
|
906
|
+
* })
|
|
1026
907
|
*
|
|
1027
|
-
*
|
|
1028
|
-
*
|
|
1029
|
-
*
|
|
1030
|
-
* schedule: '10 minutes',
|
|
1031
|
-
* })
|
|
908
|
+
* // one per config, each an independent job
|
|
909
|
+
* tasks.scheduleJob({ name: 'sync-abc', task: 'sync', payload: { configId: 'abc' }, schedule: { cron: '*\/5 * * * *' } })
|
|
910
|
+
* tasks.scheduleJob({ name: 'sync-def', task: 'sync', payload: { configId: 'def' }, schedule: { cron: '*\/5 * * * *' }, concurrency: 'parallel' })
|
|
1032
911
|
*
|
|
1033
|
-
*
|
|
912
|
+
* @remarks
|
|
913
|
+
* The registry is a plain runtime singleton — export it from one module and import
|
|
914
|
+
* it wherever you schedule or cancel jobs. Recurring jobs only arm when the module
|
|
915
|
+
* that schedules them executes, so register them from a Nitro plugin (or another
|
|
916
|
+
* startup path) if they must run from boot.
|
|
1034
917
|
*/
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
const
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
if (
|
|
1042
|
-
|
|
1043
|
-
|
|
918
|
+
function c8yTasks() {
|
|
919
|
+
const tasks = /* @__PURE__ */ new Map();
|
|
920
|
+
const jobs = /* @__PURE__ */ new Map();
|
|
921
|
+
function execute(record, override) {
|
|
922
|
+
const definition = tasks.get(record.task);
|
|
923
|
+
if (!definition) return Promise.reject(/* @__PURE__ */ new Error(`Task "${record.task}" is not registered`));
|
|
924
|
+
if (record.concurrency === "single" && record.inFlight) return record.inFlight;
|
|
925
|
+
const event = {
|
|
926
|
+
task: record.task,
|
|
927
|
+
job: record.name,
|
|
928
|
+
payload: override?.payload ?? record.payload,
|
|
929
|
+
context: override?.context ?? record.context
|
|
930
|
+
};
|
|
931
|
+
const promise = (async () => definition.handler(event))();
|
|
932
|
+
if (record.concurrency === "single") {
|
|
933
|
+
record.inFlight = promise;
|
|
934
|
+
promise.finally(() => {
|
|
935
|
+
if (record.inFlight === promise) record.inFlight = void 0;
|
|
936
|
+
});
|
|
1044
937
|
}
|
|
938
|
+
return promise;
|
|
1045
939
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
940
|
+
function toJobInfo(record) {
|
|
941
|
+
const next = record.cron.nextRun();
|
|
942
|
+
const base = {
|
|
943
|
+
name: record.name,
|
|
944
|
+
task: record.task,
|
|
945
|
+
concurrency: record.concurrency,
|
|
946
|
+
nextRun: next ? next.toISOString() : null,
|
|
947
|
+
running: record.inFlight !== void 0
|
|
948
|
+
};
|
|
949
|
+
if (record.kind === "recurring") return {
|
|
950
|
+
...base,
|
|
951
|
+
kind: "recurring",
|
|
952
|
+
cron: record.cronExpr,
|
|
953
|
+
timezone: record.timezone,
|
|
954
|
+
nextRuns: record.cron.nextRuns(3).map((date) => date.toISOString())
|
|
955
|
+
};
|
|
956
|
+
return {
|
|
957
|
+
...base,
|
|
958
|
+
kind: "once",
|
|
959
|
+
cron: null
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
function cancelJob(name) {
|
|
963
|
+
const record = jobs.get(name);
|
|
964
|
+
if (!record) return false;
|
|
965
|
+
record.cron.stop();
|
|
966
|
+
return jobs.delete(name);
|
|
967
|
+
}
|
|
968
|
+
const api = {
|
|
969
|
+
createTask(name, handler) {
|
|
970
|
+
if (typeof name !== "string" || !name) throw new TypeError("task name is required");
|
|
971
|
+
if (tasks.has(name)) throw new Error(`Task "${name}" is already registered`);
|
|
972
|
+
tasks.set(name, {
|
|
973
|
+
name,
|
|
974
|
+
handler
|
|
975
|
+
});
|
|
976
|
+
return api;
|
|
977
|
+
},
|
|
978
|
+
async run(task, options) {
|
|
979
|
+
const definition = tasks.get(task);
|
|
980
|
+
if (!definition) throw new Error(`Task "${task}" is not registered`);
|
|
981
|
+
return definition.handler({
|
|
982
|
+
task,
|
|
983
|
+
payload: options?.payload ?? {},
|
|
984
|
+
context: options?.context ?? {}
|
|
985
|
+
});
|
|
986
|
+
},
|
|
987
|
+
scheduleJob(options) {
|
|
988
|
+
const { name, task, schedule, concurrency = "single", replace = false } = options;
|
|
989
|
+
if (!name) throw new TypeError("job name is required");
|
|
990
|
+
if (!tasks.has(task)) throw new Error(`Task "${task}" is not registered. Register it with createTask() first.`);
|
|
991
|
+
if (jobs.has(name)) {
|
|
992
|
+
if (!replace) throw new Error(`Job "${name}" already exists. Pass { replace: true } to overwrite it.`);
|
|
993
|
+
cancelJob(name);
|
|
994
|
+
}
|
|
995
|
+
const isRecurring = "cron" in schedule;
|
|
996
|
+
const timezone = options.timezone ?? "UTC";
|
|
997
|
+
const record = {
|
|
998
|
+
name,
|
|
999
|
+
task,
|
|
1000
|
+
kind: isRecurring ? "recurring" : "once",
|
|
1001
|
+
concurrency,
|
|
1002
|
+
cronExpr: isRecurring ? schedule.cron : null,
|
|
1003
|
+
timezone,
|
|
1004
|
+
payload: options.payload ?? {},
|
|
1005
|
+
context: options.context ?? {},
|
|
1006
|
+
cron: void 0
|
|
1007
|
+
};
|
|
1008
|
+
const callback = () => {
|
|
1009
|
+
Promise.resolve(execute(record)).catch((error) => {
|
|
1010
|
+
console.error(`c8y-nitro: job "${name}" (task "${task}") failed`, error);
|
|
1011
|
+
}).finally(() => {
|
|
1012
|
+
if (!record.cron.nextRun()) jobs.delete(name);
|
|
1013
|
+
});
|
|
1014
|
+
};
|
|
1015
|
+
if (isRecurring) {
|
|
1016
|
+
const cronOptions = { timezone };
|
|
1017
|
+
if (typeof options.maxRuns === "number") cronOptions.maxRuns = options.maxRuns;
|
|
1018
|
+
record.cron = new Cron(schedule.cron, cronOptions, callback);
|
|
1019
|
+
} else {
|
|
1020
|
+
const fireAt = Math.max(resolveOnceDate(schedule).getTime(), Date.now() + 50);
|
|
1021
|
+
record.cron = new Cron(new Date(fireAt), { maxRuns: 1 }, callback);
|
|
1022
|
+
}
|
|
1023
|
+
jobs.set(name, record);
|
|
1024
|
+
if (isRecurring && options.immediate) Promise.resolve(execute(record)).catch((error) => {
|
|
1025
|
+
console.error(`c8y-nitro: job "${name}" immediate run failed`, error);
|
|
1026
|
+
});
|
|
1027
|
+
return toJobInfo(record);
|
|
1028
|
+
},
|
|
1029
|
+
triggerJob(name, options) {
|
|
1030
|
+
const record = jobs.get(name);
|
|
1031
|
+
if (!record) throw new Error(`Job "${name}" is not registered`);
|
|
1032
|
+
return execute(record, options);
|
|
1033
|
+
},
|
|
1034
|
+
cancelJob,
|
|
1035
|
+
listJobs() {
|
|
1036
|
+
return [...jobs.values()].map(toJobInfo);
|
|
1037
|
+
},
|
|
1038
|
+
getJob(name) {
|
|
1039
|
+
const record = jobs.get(name);
|
|
1040
|
+
return record ? toJobInfo(record) : void 0;
|
|
1041
|
+
},
|
|
1042
|
+
hasTask(name) {
|
|
1043
|
+
return tasks.has(name);
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
return api;
|
|
1049
1047
|
}
|
|
1050
1048
|
//#endregion
|
|
1051
|
-
export {
|
|
1049
|
+
export { c8yTasks, createError, createLogger, hasUserRequiredRole, isUserFromAllowedTenant, isUserFromDeployedTenant, useDeployedTenantClient, useDeployedTenantCredentials, useLogger, useSubscribedTenantClients, useSubscribedTenantCredentials, useTenantOption, useTenantOptions, useUser, useUserClient, useUserRoles, useUserTenantClient, useUserTenantCredentials };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c8y-nitro",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Lightning fast Cumulocity IoT microservice development powered by Nitro",
|
|
6
6
|
"keywords": [
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"c12": "^4.0.0-beta.5",
|
|
42
42
|
"citty": "^0.2.2",
|
|
43
43
|
"consola": "^3.4.2",
|
|
44
|
+
"croner": "^10.0.1",
|
|
44
45
|
"evlog": "^2.21.0",
|
|
45
|
-
"itty-time": "^2.0.2",
|
|
46
46
|
"jszip": "^3.10.1",
|
|
47
47
|
"pathe": "^2.0.3",
|
|
48
48
|
"pkg-types": "^2.3.1",
|