@lunora/scheduler 1.0.0-alpha.7 → 1.0.0-alpha.8

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/index.d.mts CHANGED
@@ -648,15 +648,17 @@ declare class SchedulerDO {
648
648
  *
649
649
  * Claims the job by deleting its time-index entry BEFORE dispatch (an alarm
650
650
  * re-fire then won't pick it up again), runs {@link drainRecord}, and on a
651
- * thrown storage op decides whether the job stays re-fireable.
651
+ * thrown storage op re-asserts the claim so the job stays re-fireable.
652
652
  *
653
- * When the record was NOT successfully dispatched, re-assert the time-index
654
- * claim so a later alarm re-attempts it (at-least-once): the claim delete may
655
- * have removed it and recordRetry()/requeuePooled() may not have re-armed it
656
- * before throwing, and re-inserting the same key is idempotent, so a
657
- * surviving claim is simply rewritten to its prior value. When the record WAS
658
- * dispatched (the throw came from post-dispatch cleanup), leave the index
659
- * deleted so the already-kicked, idempotent job is not re-fired.
653
+ * A throw reaching here always means the job was NOT dispatched:
654
+ * {@link drainRecord} swallows its own post-dispatch cleanup errors and
655
+ * returns instead of throwing once a kick succeeds, so every escaping throw
656
+ * comes from the pre-dispatch or failed-dispatch paths. We therefore always
657
+ * re-assert the time-index claim so a later alarm re-attempts it
658
+ * (at-least-once): the claim delete may have removed it and
659
+ * recordRetry()/requeuePooled() may not have re-armed it before throwing, and
660
+ * re-inserting the same key is idempotent, so a surviving claim is simply
661
+ * rewritten to its prior value.
660
662
  */
661
663
  private drainRecordGuarded;
662
664
  /**
@@ -666,16 +668,18 @@ declare class SchedulerDO {
666
668
  * free slot is reserved durably before dispatch and released immediately if
667
669
  * the kick fails (success holds it until the runtime reports completion).
668
670
  * Success clears the `id:`/`retry:` rows; failure routes to
669
- * {@link recordRetry}. `pools` caches each pool's {@link PoolState} for the
670
- * lifetime of the drain so the budget decrements without re-reading storage.
671
+ * {@link recordRetry}. Pool state is read FRESH from storage per record (see
672
+ * {@link reservePoolSlot}) and never held across the dispatch() await, so a
673
+ * concurrent /complete landing mid-dispatch can't be clobbered.
674
+ * Once a kick succeeds, post-dispatch cleanup (clearing the `id:`/`retry:`
675
+ * rows) is swallowed rather than allowed to throw, so a successful dispatch
676
+ * NEVER propagates an error to {@link drainRecordGuarded}: every throw that
677
+ * escapes comes from the pre-dispatch or failed-dispatch paths, where the job
678
+ * is still re-fireable and the guard safely re-claims the time index.
671
679
  * @returns `true` only when the record was successfully dispatched (a 2xx
672
- * kick). The caller ({@link drainRecordGuarded}) uses this in its per-record
673
- * error guard: a record that returns `true` (or whose post-dispatch cleanup
674
- * later throws) must NOT have its time-index claim restored, since re-firing
675
- * an already-kicked job would break idempotency. A `false` return (pool
676
- * backpressure or a failed dispatch) means the job is still re-fireable —
677
- * either already re-armed here, or, if a throw escapes, re-claimed by the
678
- * guard's catch.
680
+ * kick); `false` on pool backpressure or a failed dispatch (the job is still
681
+ * re-fireable already re-armed here). The value is informational (the guard
682
+ * branches on throw/no-throw, not on this boolean).
679
683
  */
680
684
  private drainRecord;
681
685
  /**
@@ -683,6 +687,14 @@ declare class SchedulerDO {
683
687
  * job via {@link requeuePooled}) when the pool is at `maxConcurrency`;
684
688
  * otherwise reserves a slot durably and returns `true`. Non-pooled records
685
689
  * always return `true` without touching any pool state.
690
+ *
691
+ * The pool row is read FRESH from storage on every call — never cached
692
+ * across the drain. Each reservation durably `savePool()`s before the next
693
+ * record runs, so a same-pass reservation is still visible to the next
694
+ * record's fresh read (the budget carries forward); and because dispatch()
695
+ * awaits an outbound fetch between records, a concurrent /complete that
696
+ * decrements the row mid-drain IS reflected here instead of being clobbered
697
+ * by a stale in-memory copy (which would leak a slot permanently).
686
698
  */
687
699
  private reservePoolSlot;
688
700
  /**
package/dist/index.d.ts CHANGED
@@ -648,15 +648,17 @@ declare class SchedulerDO {
648
648
  *
649
649
  * Claims the job by deleting its time-index entry BEFORE dispatch (an alarm
650
650
  * re-fire then won't pick it up again), runs {@link drainRecord}, and on a
651
- * thrown storage op decides whether the job stays re-fireable.
651
+ * thrown storage op re-asserts the claim so the job stays re-fireable.
652
652
  *
653
- * When the record was NOT successfully dispatched, re-assert the time-index
654
- * claim so a later alarm re-attempts it (at-least-once): the claim delete may
655
- * have removed it and recordRetry()/requeuePooled() may not have re-armed it
656
- * before throwing, and re-inserting the same key is idempotent, so a
657
- * surviving claim is simply rewritten to its prior value. When the record WAS
658
- * dispatched (the throw came from post-dispatch cleanup), leave the index
659
- * deleted so the already-kicked, idempotent job is not re-fired.
653
+ * A throw reaching here always means the job was NOT dispatched:
654
+ * {@link drainRecord} swallows its own post-dispatch cleanup errors and
655
+ * returns instead of throwing once a kick succeeds, so every escaping throw
656
+ * comes from the pre-dispatch or failed-dispatch paths. We therefore always
657
+ * re-assert the time-index claim so a later alarm re-attempts it
658
+ * (at-least-once): the claim delete may have removed it and
659
+ * recordRetry()/requeuePooled() may not have re-armed it before throwing, and
660
+ * re-inserting the same key is idempotent, so a surviving claim is simply
661
+ * rewritten to its prior value.
660
662
  */
661
663
  private drainRecordGuarded;
662
664
  /**
@@ -666,16 +668,18 @@ declare class SchedulerDO {
666
668
  * free slot is reserved durably before dispatch and released immediately if
667
669
  * the kick fails (success holds it until the runtime reports completion).
668
670
  * Success clears the `id:`/`retry:` rows; failure routes to
669
- * {@link recordRetry}. `pools` caches each pool's {@link PoolState} for the
670
- * lifetime of the drain so the budget decrements without re-reading storage.
671
+ * {@link recordRetry}. Pool state is read FRESH from storage per record (see
672
+ * {@link reservePoolSlot}) and never held across the dispatch() await, so a
673
+ * concurrent /complete landing mid-dispatch can't be clobbered.
674
+ * Once a kick succeeds, post-dispatch cleanup (clearing the `id:`/`retry:`
675
+ * rows) is swallowed rather than allowed to throw, so a successful dispatch
676
+ * NEVER propagates an error to {@link drainRecordGuarded}: every throw that
677
+ * escapes comes from the pre-dispatch or failed-dispatch paths, where the job
678
+ * is still re-fireable and the guard safely re-claims the time index.
671
679
  * @returns `true` only when the record was successfully dispatched (a 2xx
672
- * kick). The caller ({@link drainRecordGuarded}) uses this in its per-record
673
- * error guard: a record that returns `true` (or whose post-dispatch cleanup
674
- * later throws) must NOT have its time-index claim restored, since re-firing
675
- * an already-kicked job would break idempotency. A `false` return (pool
676
- * backpressure or a failed dispatch) means the job is still re-fireable —
677
- * either already re-armed here, or, if a throw escapes, re-claimed by the
678
- * guard's catch.
680
+ * kick); `false` on pool backpressure or a failed dispatch (the job is still
681
+ * re-fireable already re-armed here). The value is informational (the guard
682
+ * branches on throw/no-throw, not on this boolean).
679
683
  */
680
684
  private drainRecord;
681
685
  /**
@@ -683,6 +687,14 @@ declare class SchedulerDO {
683
687
  * job via {@link requeuePooled}) when the pool is at `maxConcurrency`;
684
688
  * otherwise reserves a slot durably and returns `true`. Non-pooled records
685
689
  * always return `true` without touching any pool state.
690
+ *
691
+ * The pool row is read FRESH from storage on every call — never cached
692
+ * across the drain. Each reservation durably `savePool()`s before the next
693
+ * record runs, so a same-pass reservation is still visible to the next
694
+ * record's fresh read (the budget carries forward); and because dispatch()
695
+ * awaits an outbound fetch between records, a concurrent /complete that
696
+ * decrements the row mid-drain IS reflected here instead of being clobbered
697
+ * by a stale in-memory copy (which would leak a slot permanently).
686
698
  */
687
699
  private reservePoolSlot;
688
700
  /**
package/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
- export { default as createScheduler } from './packem_shared/createScheduler-CcD09oIm.mjs';
2
- export { default as createWorkpool } from './packem_shared/createWorkpool-C28trhTA.mjs';
1
+ export { default as createScheduler } from './packem_shared/createScheduler-DGVqs2Ft.mjs';
2
+ export { default as createWorkpool } from './packem_shared/createWorkpool-Ce0kNM2p.mjs';
3
3
  export { createCronTrigger } from './packem_shared/createCronTrigger-Dh4VLxD9.mjs';
4
- export { CRON_SCHEDULE_KINDS, compileCronSchedule, cronJobs } from './packem_shared/CRON_SCHEDULE_KINDS-BMVeCHOu.mjs';
4
+ export { CRON_SCHEDULE_KINDS, compileCronSchedule, cronJobs } from './packem_shared/CRON_SCHEDULE_KINDS-C2FflEnq.mjs';
5
5
  export { createQueueConsumer, createQueueWorkpool, httpDispatcher } from './packem_shared/createQueueConsumer-Cy-Mp-El.mjs';
6
- export { SchedulerDO } from './packem_shared/SchedulerDO-DVeJrNbs.mjs';
6
+ export { SchedulerDO } from './packem_shared/SchedulerDO-DbXUK1Qa.mjs';
7
7
  export { isWorkflowReference } from './packem_shared/isWorkflowReference-C9mQkMXt.mjs';
8
8
  export { assertValidCronExpression, isValidCronExpression } from './packem_shared/assertValidCronExpression-B9m75qU0.mjs';
@@ -20,6 +20,16 @@ const field = (value, label, min, max) => {
20
20
  }
21
21
  return value.toFixed(0);
22
22
  };
23
+ const stepField = (value, label, period) => {
24
+ const rendered = field(value, label, 1, period - 1);
25
+ if (period % value !== 0) {
26
+ throw new LunoraError(
27
+ "INTERNAL",
28
+ `@lunora/scheduler: ${label} must evenly divide ${period.toFixed(0)} for a fixed "every ${value.toFixed(0)}" interval — cron "*/${value.toFixed(0)}" means "at values divisible by ${value.toFixed(0)}", which wraps unevenly; pick a divisor of ${period.toFixed(0)}`
29
+ );
30
+ }
31
+ return rendered;
32
+ };
23
33
  const compileInterval = (schedule) => {
24
34
  const units = ["seconds", "minutes", "hours"].filter((unit2) => schedule[unit2] !== void 0);
25
35
  if (units.length !== 1) {
@@ -28,12 +38,12 @@ const compileInterval = (schedule) => {
28
38
  const unit = units[0];
29
39
  const value = schedule[unit];
30
40
  if (unit === "seconds") {
31
- return `*/${field(value, "interval.seconds", 1, 59)} * * * * *`;
41
+ return `*/${stepField(value, "interval.seconds", 60)} * * * * *`;
32
42
  }
33
43
  if (unit === "minutes") {
34
- return `*/${field(value, "interval.minutes", 1, 59)} * * * *`;
44
+ return `*/${stepField(value, "interval.minutes", 60)} * * * *`;
35
45
  }
36
- return `0 */${field(value, "interval.hours", 1, 23)} * * *`;
46
+ return `0 */${stepField(value, "interval.hours", 24)} * * *`;
37
47
  };
38
48
  const compileDaily = (schedule) => {
39
49
  const minute = field(schedule.minuteUTC, "daily.minuteUTC", 0, 59);
@@ -7,7 +7,7 @@ const POOL_PREFIX = "pool:";
7
7
  const MAX_RETRY_ATTEMPTS = 5;
8
8
  const RETRY_BASE_DELAY_MS = 3e4;
9
9
  const POOL_BACKPRESSURE_DELAY_MS = 1e3;
10
- const MAX_SCHEDULED_FOR_MS = 864e13;
10
+ const MAX_SCHEDULED_FOR_MS = 999999999999999;
11
11
  const TIME_PAD = 15;
12
12
  const padTime = (n) => String(n).padStart(TIME_PAD, "0");
13
13
  const generateId = () => {
@@ -162,13 +162,14 @@ class SchedulerDO {
162
162
  const record = await this.state.storage.get(`${HEADER_PREFIX}${recordId}`);
163
163
  if (record) {
164
164
  due.push(record);
165
+ } else {
166
+ await this.state.storage.delete(indexKey);
165
167
  }
166
168
  }
167
169
  }
168
- const pools = /* @__PURE__ */ new Map();
169
170
  try {
170
171
  for (const record of due) {
171
- await this.drainRecordGuarded(record, pools);
172
+ await this.drainRecordGuarded(record);
172
173
  }
173
174
  } finally {
174
175
  await this.rescheduleAlarm();
@@ -237,27 +238,26 @@ class SchedulerDO {
237
238
  *
238
239
  * Claims the job by deleting its time-index entry BEFORE dispatch (an alarm
239
240
  * re-fire then won't pick it up again), runs {@link drainRecord}, and on a
240
- * thrown storage op decides whether the job stays re-fireable.
241
+ * thrown storage op re-asserts the claim so the job stays re-fireable.
241
242
  *
242
- * When the record was NOT successfully dispatched, re-assert the time-index
243
- * claim so a later alarm re-attempts it (at-least-once): the claim delete may
244
- * have removed it and recordRetry()/requeuePooled() may not have re-armed it
245
- * before throwing, and re-inserting the same key is idempotent, so a
246
- * surviving claim is simply rewritten to its prior value. When the record WAS
247
- * dispatched (the throw came from post-dispatch cleanup), leave the index
248
- * deleted so the already-kicked, idempotent job is not re-fired.
243
+ * A throw reaching here always means the job was NOT dispatched:
244
+ * {@link drainRecord} swallows its own post-dispatch cleanup errors and
245
+ * returns instead of throwing once a kick succeeds, so every escaping throw
246
+ * comes from the pre-dispatch or failed-dispatch paths. We therefore always
247
+ * re-assert the time-index claim so a later alarm re-attempts it
248
+ * (at-least-once): the claim delete may have removed it and
249
+ * recordRetry()/requeuePooled() may not have re-armed it before throwing, and
250
+ * re-inserting the same key is idempotent, so a surviving claim is simply
251
+ * rewritten to its prior value.
249
252
  */
250
- async drainRecordGuarded(record, pools) {
251
- let dispatched = false;
253
+ async drainRecordGuarded(record) {
252
254
  try {
253
255
  await this.state.storage.delete(SchedulerDO.indexKey(record.scheduledFor, record.id));
254
- dispatched = await this.drainRecord(record, pools);
256
+ await this.drainRecord(record);
255
257
  } catch {
256
- if (!dispatched) {
257
- try {
258
- await this.state.storage.put(SchedulerDO.indexKey(record.scheduledFor, record.id), record.id);
259
- } catch {
260
- }
258
+ try {
259
+ await this.state.storage.put(SchedulerDO.indexKey(record.scheduledFor, record.id), record.id);
260
+ } catch {
261
261
  }
262
262
  }
263
263
  }
@@ -268,30 +268,29 @@ class SchedulerDO {
268
268
  * free slot is reserved durably before dispatch and released immediately if
269
269
  * the kick fails (success holds it until the runtime reports completion).
270
270
  * Success clears the `id:`/`retry:` rows; failure routes to
271
- * {@link recordRetry}. `pools` caches each pool's {@link PoolState} for the
272
- * lifetime of the drain so the budget decrements without re-reading storage.
271
+ * {@link recordRetry}. Pool state is read FRESH from storage per record (see
272
+ * {@link reservePoolSlot}) and never held across the dispatch() await, so a
273
+ * concurrent /complete landing mid-dispatch can't be clobbered.
274
+ * Once a kick succeeds, post-dispatch cleanup (clearing the `id:`/`retry:`
275
+ * rows) is swallowed rather than allowed to throw, so a successful dispatch
276
+ * NEVER propagates an error to {@link drainRecordGuarded}: every throw that
277
+ * escapes comes from the pre-dispatch or failed-dispatch paths, where the job
278
+ * is still re-fireable and the guard safely re-claims the time index.
273
279
  * @returns `true` only when the record was successfully dispatched (a 2xx
274
- * kick). The caller ({@link drainRecordGuarded}) uses this in its per-record
275
- * error guard: a record that returns `true` (or whose post-dispatch cleanup
276
- * later throws) must NOT have its time-index claim restored, since re-firing
277
- * an already-kicked job would break idempotency. A `false` return (pool
278
- * backpressure or a failed dispatch) means the job is still re-fireable —
279
- * either already re-armed here, or, if a throw escapes, re-claimed by the
280
- * guard's catch.
280
+ * kick); `false` on pool backpressure or a failed dispatch (the job is still
281
+ * re-fireable already re-armed here). The value is informational (the guard
282
+ * branches on throw/no-throw, not on this boolean).
281
283
  */
282
- async drainRecord(record, pools) {
283
- const reserved = await this.reservePoolSlot(record, pools);
284
+ async drainRecord(record) {
285
+ const reserved = await this.reservePoolSlot(record);
284
286
  if (!reserved) {
285
287
  return false;
286
288
  }
287
289
  const ok = await this.dispatch(record);
288
290
  if (!ok && record.pool !== void 0) {
289
- const pool = pools.get(record.pool);
290
- if (pool !== void 0) {
291
- const released = SchedulerDO.releaseSlot(pool, record.id);
292
- pools.set(record.pool, released);
293
- await this.savePool(record.pool, released);
294
- }
291
+ const pool = await this.loadPool(record.pool);
292
+ const released = SchedulerDO.releaseSlot(pool, record.id);
293
+ await this.savePool(record.pool, released);
295
294
  }
296
295
  if (ok) {
297
296
  try {
@@ -308,13 +307,20 @@ class SchedulerDO {
308
307
  * job via {@link requeuePooled}) when the pool is at `maxConcurrency`;
309
308
  * otherwise reserves a slot durably and returns `true`. Non-pooled records
310
309
  * always return `true` without touching any pool state.
310
+ *
311
+ * The pool row is read FRESH from storage on every call — never cached
312
+ * across the drain. Each reservation durably `savePool()`s before the next
313
+ * record runs, so a same-pass reservation is still visible to the next
314
+ * record's fresh read (the budget carries forward); and because dispatch()
315
+ * awaits an outbound fetch between records, a concurrent /complete that
316
+ * decrements the row mid-drain IS reflected here instead of being clobbered
317
+ * by a stale in-memory copy (which would leak a slot permanently).
311
318
  */
312
- async reservePoolSlot(record, pools) {
319
+ async reservePoolSlot(record) {
313
320
  if (record.pool === void 0) {
314
321
  return true;
315
322
  }
316
- const pool = pools.get(record.pool) ?? await this.loadPool(record.pool);
317
- pools.set(record.pool, pool);
323
+ const pool = await this.loadPool(record.pool);
318
324
  if (pool.inFlight >= pool.maxConcurrency) {
319
325
  await this.requeuePooled(record);
320
326
  return false;
@@ -522,7 +528,7 @@ class SchedulerDO {
522
528
  return SchedulerDO.error(400, "INVALID_INPUT", "functionPath is required");
523
529
  }
524
530
  if (typeof body.scheduledFor !== "number" || !Number.isInteger(body.scheduledFor) || body.scheduledFor <= 0 || body.scheduledFor > MAX_SCHEDULED_FOR_MS) {
525
- return SchedulerDO.error(400, "INVALID_INPUT", "scheduledFor must be a positive integer epoch-millisecond number no greater than 8640000000000000");
531
+ return SchedulerDO.error(400, "INVALID_INPUT", "scheduledFor must be a positive integer epoch-millisecond number no greater than 999999999999999");
526
532
  }
527
533
  if (typeof this.env.LUNORA_ORIGIN_URL !== "string" || this.env.LUNORA_ORIGIN_URL.length === 0) {
528
534
  return SchedulerDO.error(500, "ORIGIN_NOT_CONFIGURED", "LUNORA_ORIGIN_URL env binding must be set on the SchedulerDO");
@@ -1,32 +1,6 @@
1
1
  import { LunoraError } from '@lunora/errors';
2
- import { a as applyJurisdiction } from './jurisdiction-CR2zC3Et.mjs';
2
+ import { c as callDO, g as getDO } from './do-client-CMJtLoHO.mjs';
3
3
 
4
- const schedulerStub = (options) => {
5
- const namespace = applyJurisdiction(options.namespace, options.jurisdiction);
6
- return namespace.get(namespace.idFromName(options.instanceName ?? "default"));
7
- };
8
- const callDO = async (options, path, body) => {
9
- const stub = schedulerStub(options);
10
- const response = await stub.fetch(`https://scheduler.internal${path}`, {
11
- body: JSON.stringify(body),
12
- headers: { "content-type": "application/json" },
13
- method: "POST"
14
- });
15
- if (!response.ok) {
16
- const text = await response.text();
17
- throw new LunoraError("INTERNAL", `@lunora/scheduler: SchedulerDO ${path} failed (${String(response.status)}): ${text}`);
18
- }
19
- return await response.json();
20
- };
21
- const getDO = async (options, path) => {
22
- const stub = schedulerStub(options);
23
- const response = await stub.fetch(`https://scheduler.internal${path}`, { method: "GET" });
24
- if (!response.ok) {
25
- const text = await response.text();
26
- throw new LunoraError("INTERNAL", `@lunora/scheduler: SchedulerDO ${path} failed (${String(response.status)}): ${text}`);
27
- }
28
- return await response.json();
29
- };
30
4
  const createScheduler = (options) => {
31
5
  if (!options.namespace) {
32
6
  throw new LunoraError("INTERNAL", "@lunora/scheduler: `namespace` (SchedulerDO binding) is required");
@@ -1,32 +1,6 @@
1
1
  import { LunoraError } from '@lunora/errors';
2
- import { a as applyJurisdiction } from './jurisdiction-CR2zC3Et.mjs';
2
+ import { g as getDO, c as callDO } from './do-client-CMJtLoHO.mjs';
3
3
 
4
- const workpoolStub = (options) => {
5
- const namespace = applyJurisdiction(options.namespace, options.jurisdiction);
6
- return namespace.get(namespace.idFromName(options.instanceName ?? "default"));
7
- };
8
- const callDO = async (options, path, body) => {
9
- const stub = workpoolStub(options);
10
- const response = await stub.fetch(`https://scheduler.internal${path}`, {
11
- body: JSON.stringify(body),
12
- headers: { "content-type": "application/json" },
13
- method: "POST"
14
- });
15
- if (!response.ok) {
16
- const text = await response.text();
17
- throw new LunoraError("INTERNAL", `@lunora/scheduler: SchedulerDO ${path} failed (${String(response.status)}): ${text}`);
18
- }
19
- return await response.json();
20
- };
21
- const getDO = async (options, path) => {
22
- const stub = workpoolStub(options);
23
- const response = await stub.fetch(`https://scheduler.internal${path}`, { method: "GET" });
24
- if (!response.ok) {
25
- const text = await response.text();
26
- throw new LunoraError("INTERNAL", `@lunora/scheduler: SchedulerDO ${path} failed (${String(response.status)}): ${text}`);
27
- }
28
- return await response.json();
29
- };
30
4
  const createWorkpool = (options) => {
31
5
  if (!options.namespace) {
32
6
  throw new LunoraError("INTERNAL", "@lunora/scheduler: `namespace` (SchedulerDO binding) is required");
@@ -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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/scheduler",
3
- "version": "1.0.0-alpha.7",
3
+ "version": "1.0.0-alpha.8",
4
4
  "description": "Scheduling for Lunora: runAfter / runAt and Cron Triggers via SchedulerDO",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -46,7 +46,7 @@
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
- "@lunora/errors": "1.0.0-alpha.3",
49
+ "@lunora/errors": "1.0.0-alpha.4",
50
50
  "cron-parser": "5.6.1"
51
51
  },
52
52
  "engines": {
@@ -1,13 +0,0 @@
1
- const applyJurisdiction = (namespace, jurisdiction) => {
2
- if (jurisdiction === void 0) {
3
- return namespace;
4
- }
5
- if (typeof namespace.jurisdiction !== "function") {
6
- throw new TypeError(
7
- `@lunora/scheduler: Durable Object namespace does not support jurisdiction("${jurisdiction}") — update @cloudflare/workers-types or remove the jurisdiction option`
8
- );
9
- }
10
- return namespace.jurisdiction(jurisdiction);
11
- };
12
-
13
- export { applyJurisdiction as a };