@lunora/scheduler 0.0.0 → 1.0.0-alpha.10

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.
@@ -0,0 +1,24 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+ import { CronExpressionParser } from 'cron-parser';
3
+
4
+ const isValidCronExpression = (schedule) => {
5
+ if (typeof schedule !== "string" || schedule.trim() === "") {
6
+ return false;
7
+ }
8
+ try {
9
+ CronExpressionParser.parse(schedule.trim());
10
+ return true;
11
+ } catch {
12
+ return false;
13
+ }
14
+ };
15
+ const assertValidCronExpression = (schedule, context = "cron expression") => {
16
+ if (!isValidCronExpression(schedule)) {
17
+ throw new LunoraError(
18
+ "INTERNAL",
19
+ `@lunora/scheduler: invalid ${context} "${schedule}" — expected a standard 5- or 6-field cron expression (e.g. "0 * * * *")`
20
+ );
21
+ }
22
+ };
23
+
24
+ export { assertValidCronExpression, isValidCronExpression };
@@ -0,0 +1,28 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+ import { assertValidCronExpression } from './assertValidCronExpression-B9m75qU0.mjs';
3
+
4
+ const createCronTrigger = (options) => {
5
+ if (!options.schedule || !options.fn) {
6
+ throw new LunoraError("INTERNAL", "@lunora/scheduler: createCronTrigger() requires `schedule` and `fn`");
7
+ }
8
+ assertValidCronExpression(options.schedule);
9
+ const snippet = JSON.stringify(
10
+ {
11
+ triggers: {
12
+ crons: [options.schedule]
13
+ }
14
+ },
15
+ void 0,
16
+ 2
17
+ );
18
+ return {
19
+ crons: [options.schedule],
20
+ dispatcher: {
21
+ args: options.args ?? {},
22
+ functionPath: options.fn.__lunoraRef
23
+ },
24
+ wranglerJsonc: snippet
25
+ };
26
+ };
27
+
28
+ export { createCronTrigger };
@@ -0,0 +1,61 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+
3
+ const trimTrailingSlashes = (value) => {
4
+ let end = value.length;
5
+ while (end > 0 && value[end - 1] === "/") {
6
+ end -= 1;
7
+ }
8
+ return value.slice(0, end);
9
+ };
10
+ const createQueueWorkpool = (options) => {
11
+ if (!options.queue) {
12
+ throw new LunoraError("INTERNAL", "@lunora/scheduler: `queue` (a Cloudflare Queue binding) is required");
13
+ }
14
+ const enqueue = async (function_, args, enqueueOptions = {}) => {
15
+ const job = { args, functionPath: function_.__lunoraRef, shardKey: enqueueOptions.shardKey };
16
+ const sendOptions = enqueueOptions.delaySeconds === void 0 ? void 0 : { delaySeconds: enqueueOptions.delaySeconds };
17
+ await options.queue.send(job, sendOptions);
18
+ };
19
+ const enqueueBatch = async (jobs, sendOptions) => {
20
+ const messages = jobs.map((job) => {
21
+ return { body: { args: job.args, functionPath: job.ref.__lunoraRef, shardKey: job.shardKey } };
22
+ });
23
+ await options.queue.sendBatch(messages, sendOptions);
24
+ };
25
+ return { enqueue, enqueueBatch };
26
+ };
27
+ const isQueueJob = (value) => typeof value === "object" && value !== null && typeof value.functionPath === "string";
28
+ const createQueueConsumer = (options) => async (batch) => {
29
+ await Promise.all(
30
+ batch.messages.map(async (message) => {
31
+ try {
32
+ if (!isQueueJob(message.body)) {
33
+ throw new LunoraError("INTERNAL", "@lunora/scheduler: queue message body is not a QueueJob (missing functionPath)");
34
+ }
35
+ await options.dispatch(message.body);
36
+ message.ack();
37
+ } catch {
38
+ message.retry();
39
+ }
40
+ })
41
+ );
42
+ };
43
+ const httpDispatcher = (options) => {
44
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
45
+ if (typeof fetchImpl !== "function") {
46
+ throw new TypeError("@lunora/scheduler: no fetch implementation available — pass fetchImpl or run on a platform with global fetch");
47
+ }
48
+ const url = `${trimTrailingSlashes(options.originUrl)}/_lunora/scheduler/dispatch`;
49
+ return async (job) => {
50
+ const response = await fetchImpl(url, {
51
+ body: JSON.stringify({ args: job.args ?? {}, functionPath: job.functionPath, shardKey: job.shardKey }),
52
+ headers: { authorization: `Bearer ${options.adminToken}`, "content-type": "application/json" },
53
+ method: "POST"
54
+ });
55
+ if (!response.ok) {
56
+ throw new LunoraError("INTERNAL", `@lunora/scheduler: queue dispatch failed (${response.status.toString()}): ${await response.text()}`);
57
+ }
58
+ };
59
+ };
60
+
61
+ export { createQueueConsumer, createQueueWorkpool, httpDispatcher };
@@ -0,0 +1,52 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+ import { c as callDO, g as getDO } from './do-client-CMJtLoHO.mjs';
3
+ import { isWorkflowReference } from './isWorkflowReference-C9mQkMXt.mjs';
4
+
5
+ const createScheduler = (options) => {
6
+ if (!options.namespace) {
7
+ throw new LunoraError("INTERNAL", "@lunora/scheduler: `namespace` (SchedulerDO binding) is required");
8
+ }
9
+ if (!options.originUrl) {
10
+ throw new LunoraError("INTERNAL", "@lunora/scheduler: `originUrl` is required so the DO can dispatch back to the Worker");
11
+ }
12
+ const runAt = async (date, target, args, options_ = {}) => {
13
+ const scheduledFor = date instanceof Date ? date.getTime() : date;
14
+ const base = {
15
+ args,
16
+ originUrl: options.originUrl,
17
+ pool: options_.pool,
18
+ retry: options_.retry,
19
+ scheduledFor,
20
+ shardKey: options_.shardKey
21
+ };
22
+ if (isWorkflowReference(target)) {
23
+ if (typeof target.binding !== "string" || target.binding.length === 0) {
24
+ throw new LunoraError(
25
+ "INTERNAL",
26
+ "@lunora/scheduler: workflow/agent schedule target is missing its `binding` — pass the generated `workflows.<name>` / `agents.<name>` reference"
27
+ );
28
+ }
29
+ return callDO(options, "/schedule", { ...base, workflow: target.binding });
30
+ }
31
+ const functionPath = typeof target === "string" ? target : target.__lunoraRef;
32
+ return callDO(options, "/schedule", { ...base, functionPath });
33
+ };
34
+ const runAfter = async (delayMs, target, args, options_ = {}) => {
35
+ if (!Number.isFinite(delayMs) || delayMs < 0) {
36
+ throw new LunoraError("INTERNAL", "@lunora/scheduler: `delayMs` must be a non-negative finite number");
37
+ }
38
+ return runAt(Date.now() + delayMs, target, args, options_);
39
+ };
40
+ const cancel = async (id) => callDO(options, "/cancel", { id });
41
+ const list = async () => {
42
+ const body = await getDO(options, "/list");
43
+ return Array.isArray(body.records) ? body.records : [];
44
+ };
45
+ const get = async (id) => {
46
+ const body = await getDO(options, `/get?id=${encodeURIComponent(id)}`);
47
+ return body.record ?? null;
48
+ };
49
+ return { cancel, get, list, runAfter, runAt };
50
+ };
51
+
52
+ export { createScheduler as default };
@@ -0,0 +1,37 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+ import { g as getDO, c as callDO } from './do-client-CMJtLoHO.mjs';
3
+
4
+ const createWorkpool = (options) => {
5
+ if (!options.namespace) {
6
+ throw new LunoraError("INTERNAL", "@lunora/scheduler: `namespace` (SchedulerDO binding) is required");
7
+ }
8
+ if (!options.originUrl) {
9
+ throw new LunoraError("INTERNAL", "@lunora/scheduler: `originUrl` is required so the DO can dispatch back to the Worker");
10
+ }
11
+ if (!Number.isInteger(options.maxConcurrency) || options.maxConcurrency <= 0) {
12
+ throw new LunoraError("INTERNAL", "@lunora/scheduler: `maxConcurrency` must be a positive integer");
13
+ }
14
+ const name = typeof options.name === "string" && options.name.length > 0 ? options.name : "default";
15
+ const enqueue = async (function_, args, options_ = {}) => {
16
+ const delayMs = options_.delayMs ?? 0;
17
+ if (!Number.isFinite(delayMs) || delayMs < 0) {
18
+ throw new LunoraError("INTERNAL", "@lunora/scheduler: `delayMs` must be a non-negative finite number");
19
+ }
20
+ return callDO(options, "/schedule", {
21
+ args,
22
+ functionPath: function_.__lunoraRef,
23
+ instanceName: options.instanceName ?? "default",
24
+ maxConcurrency: options.maxConcurrency,
25
+ originUrl: options.originUrl,
26
+ pool: name,
27
+ retry: options_.retry,
28
+ scheduledFor: Date.now() + delayMs,
29
+ shardKey: options_.shardKey
30
+ });
31
+ };
32
+ const cancel = async (id) => callDO(options, "/cancel", { id });
33
+ const status = async () => getDO(options, `/pool?name=${encodeURIComponent(name)}`);
34
+ return { cancel, enqueue, name, status };
35
+ };
36
+
37
+ export { createWorkpool as default };
@@ -0,0 +1,42 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+
3
+ const applyJurisdiction = (namespace, jurisdiction) => {
4
+ if (jurisdiction === void 0) {
5
+ return namespace;
6
+ }
7
+ if (typeof namespace.jurisdiction !== "function") {
8
+ throw new TypeError(
9
+ `@lunora/scheduler: Durable Object namespace does not support jurisdiction("${jurisdiction}") — update @cloudflare/workers-types or remove the jurisdiction option`
10
+ );
11
+ }
12
+ return namespace.jurisdiction(jurisdiction);
13
+ };
14
+
15
+ const schedulerStub = (options) => {
16
+ const namespace = applyJurisdiction(options.namespace, options.jurisdiction);
17
+ return namespace.get(namespace.idFromName(options.instanceName ?? "default"));
18
+ };
19
+ const callDO = async (options, path, body) => {
20
+ const stub = schedulerStub(options);
21
+ const response = await stub.fetch(`https://scheduler.internal${path}`, {
22
+ body: JSON.stringify(body),
23
+ headers: { "content-type": "application/json" },
24
+ method: "POST"
25
+ });
26
+ if (!response.ok) {
27
+ const text = await response.text();
28
+ throw new LunoraError("INTERNAL", `@lunora/scheduler: SchedulerDO ${path} failed (${String(response.status)}): ${text}`);
29
+ }
30
+ return await response.json();
31
+ };
32
+ const getDO = async (options, path) => {
33
+ const stub = schedulerStub(options);
34
+ const response = await stub.fetch(`https://scheduler.internal${path}`, { method: "GET" });
35
+ if (!response.ok) {
36
+ const text = await response.text();
37
+ throw new LunoraError("INTERNAL", `@lunora/scheduler: SchedulerDO ${path} failed (${String(response.status)}): ${text}`);
38
+ }
39
+ return await response.json();
40
+ };
41
+
42
+ export { callDO as c, getDO as g };
@@ -0,0 +1,3 @@
1
+ const isWorkflowReference = (target) => typeof target === "object" && target !== null && target.isLunoraWorkflow === true;
2
+
3
+ export { isWorkflowReference };
package/package.json CHANGED
@@ -1,31 +1,55 @@
1
1
  {
2
2
  "name": "@lunora/scheduler",
3
- "version": "0.0.0",
3
+ "version": "1.0.0-alpha.10",
4
4
  "description": "Scheduling for Lunora: runAfter / runAt and Cron Triggers via SchedulerDO",
5
- "license": "FSL-1.1-Apache-2.0",
5
+ "keywords": [
6
+ "cloudflare",
7
+ "cron",
8
+ "durable-objects",
9
+ "lunora",
10
+ "queues",
11
+ "scheduler",
12
+ "workers",
13
+ "workpool"
14
+ ],
6
15
  "homepage": "https://lunora.sh",
16
+ "bugs": "https://github.com/anolilab/lunora/issues",
17
+ "license": "FSL-1.1-Apache-2.0",
18
+ "author": {
19
+ "name": "Daniel Bannert",
20
+ "email": "d.bannert@anolilab.de"
21
+ },
7
22
  "repository": {
8
23
  "type": "git",
9
24
  "url": "git+https://github.com/anolilab/lunora.git",
10
25
  "directory": "packages/scheduler"
11
26
  },
12
- "bugs": {
13
- "url": "https://github.com/anolilab/lunora/issues"
14
- },
15
- "keywords": [
16
- "lunora",
17
- "cloudflare",
18
- "workers",
19
- "durable-objects",
20
- "scheduler",
21
- "cron",
22
- "queues",
23
- "workpool"
27
+ "files": [
28
+ "./dist",
29
+ "__assets__",
30
+ "README.md",
31
+ "LICENSE.md"
24
32
  ],
33
+ "type": "module",
34
+ "sideEffects": false,
35
+ "main": "./dist/index.mjs",
36
+ "module": "./dist/index.mjs",
37
+ "types": "./dist/index.d.ts",
38
+ "exports": {
39
+ ".": {
40
+ "types": "./dist/index.d.ts",
41
+ "import": "./dist/index.mjs"
42
+ },
43
+ "./package.json": "./package.json"
44
+ },
25
45
  "publishConfig": {
26
46
  "access": "public"
27
47
  },
28
- "files": [
29
- "README.md"
30
- ]
48
+ "dependencies": {
49
+ "@lunora/errors": "1.0.0-alpha.5",
50
+ "cron-parser": "5.6.1"
51
+ },
52
+ "engines": {
53
+ "node": "^22.15.0 || >=24.11.0"
54
+ }
31
55
  }