@sentry/junior-scheduler 0.75.0 → 0.76.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/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { createSchedulerPlugin, schedulerPlugin } from "./plugin";
2
2
  export { createSlackScheduleCreateTaskTool, createSlackScheduleDeleteTaskTool, createSlackScheduleListTasksTool, createSlackScheduleRunTaskNowTool, createSlackScheduleUpdateTaskTool, type SchedulerToolContext, } from "./schedule-tools";
3
- export { createSchedulerOperationalSqlStore, createSchedulerSqlStore, migrateSchedulerStateToSql, } from "./store";
3
+ export { createSchedulerOperationalSqlStore, createSchedulerSqlStore, migrateSchedulerStateToSql, type SchedulerDb, } from "./store";
4
4
  export type { ScheduledCalendarFrequency, ScheduledLocalTime, ScheduledRun, ScheduledRunStatus, ScheduledTask, ScheduledTaskConversationAccess, ScheduledTaskExecutionActor, ScheduledTaskPrincipal, ScheduledTaskRecurrence, ScheduledTaskSchedule, ScheduledTaskSpec, ScheduledTaskStatus, } from "./types";
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // src/plugin.ts
2
2
  import {
3
+ createSlackSource,
3
4
  defineJuniorPlugin
4
5
  } from "@sentry/junior-plugin-api";
5
6
 
@@ -9,6 +10,17 @@ import {
9
10
  destinationSchema,
10
11
  isSlackDestination
11
12
  } from "@sentry/junior-plugin-api";
13
+ import {
14
+ and,
15
+ asc,
16
+ eq,
17
+ inArray,
18
+ isNotNull,
19
+ lte,
20
+ ne,
21
+ or,
22
+ sql as sql2
23
+ } from "drizzle-orm";
12
24
  import { z } from "zod";
13
25
 
14
26
  // src/cadence.ts
@@ -97,7 +109,7 @@ function localDateTimeToTimestampMs(args) {
97
109
  0
98
110
  );
99
111
  let timestampMs = localAsUtcMs - getTimeZoneOffsetMs(localAsUtcMs, args.timezone);
100
- for (let index = 0; index < 3; index += 1) {
112
+ for (let index2 = 0; index2 < 3; index2 += 1) {
101
113
  const next = localAsUtcMs - getTimeZoneOffsetMs(timestampMs, args.timezone);
102
114
  if (next === timestampMs) {
103
115
  break;
@@ -358,6 +370,52 @@ function getNextRunAtMs(task, scheduledForMs, afterMs = scheduledForMs) {
358
370
  });
359
371
  }
360
372
 
373
+ // src/db/schema.ts
374
+ import { sql } from "drizzle-orm";
375
+ import { bigint, index, jsonb, pgTable, text } from "drizzle-orm/pg-core";
376
+ var juniorSchedulerTasks = pgTable(
377
+ "junior_scheduler_tasks",
378
+ {
379
+ id: text("id").primaryKey(),
380
+ teamId: text("team_id").notNull(),
381
+ status: text("status").notNull(),
382
+ nextRunAtMs: bigint("next_run_at_ms", { mode: "number" }),
383
+ runNowAtMs: bigint("run_now_at_ms", { mode: "number" }),
384
+ createdAtMs: bigint("created_at_ms", { mode: "number" }).notNull(),
385
+ record: jsonb("record").$type().notNull()
386
+ },
387
+ (table) => [
388
+ index("junior_scheduler_tasks_team_status_idx").on(table.teamId, table.createdAtMs, table.id).where(sql`${table.status} <> 'deleted'`),
389
+ index("junior_scheduler_tasks_run_now_due_idx").on(table.runNowAtMs, table.createdAtMs, table.id).where(
390
+ sql`${table.status} = 'active' AND ${table.runNowAtMs} IS NOT NULL`
391
+ ),
392
+ index("junior_scheduler_tasks_next_run_due_idx").on(table.nextRunAtMs, table.createdAtMs, table.id).where(
393
+ sql`${table.status} = 'active' AND ${table.nextRunAtMs} IS NOT NULL`
394
+ )
395
+ ]
396
+ );
397
+ var juniorSchedulerRuns = pgTable(
398
+ "junior_scheduler_runs",
399
+ {
400
+ id: text("id").primaryKey(),
401
+ taskId: text("task_id").notNull(),
402
+ status: text("status").notNull(),
403
+ scheduledForMs: bigint("scheduled_for_ms", { mode: "number" }).notNull(),
404
+ record: jsonb("record").$type().notNull()
405
+ },
406
+ (table) => [
407
+ index("junior_scheduler_runs_task_status_idx").on(
408
+ table.taskId,
409
+ table.status,
410
+ table.scheduledForMs
411
+ ),
412
+ index("junior_scheduler_runs_status_idx").on(
413
+ table.status,
414
+ table.scheduledForMs
415
+ )
416
+ ]
417
+ );
418
+
361
419
  // src/store.ts
362
420
  var SCHEDULER_KEY_PREFIX = "junior:scheduler";
363
421
  var SCHEDULER_RECORD_TTL_MS = 5 * 365 * 24 * 60 * 60 * 1e3;
@@ -616,111 +674,74 @@ function parseSqlRunRow(row) {
616
674
  const parsed = runRecordSchema.safeParse(parseJsonRecord(row.record));
617
675
  return parsed.success ? stripLegacyRunFields(parsed.data) : void 0;
618
676
  }
619
- function json(value) {
620
- return JSON.stringify(value);
621
- }
622
677
  async function withSqlLock(db, key, callback) {
623
678
  return await db.transaction(async (tx) => {
624
- await tx.execute("SELECT pg_advisory_xact_lock(hashtext($1))", [key]);
679
+ await tx.execute(sql2`SELECT pg_advisory_xact_lock(hashtext(${key}))`);
625
680
  return await callback(tx);
626
681
  });
627
682
  }
628
683
  async function upsertSqlTask(db, task) {
629
- await db.execute(
630
- `
631
- INSERT INTO junior_scheduler_tasks (
632
- id,
633
- team_id,
634
- status,
635
- next_run_at_ms,
636
- run_now_at_ms,
637
- created_at_ms,
638
- record
639
- ) VALUES (
640
- $1, $2, $3, $4, $5, $6, $7::jsonb
641
- )
642
- ON CONFLICT (id) DO UPDATE SET
643
- team_id = EXCLUDED.team_id,
644
- status = EXCLUDED.status,
645
- next_run_at_ms = EXCLUDED.next_run_at_ms,
646
- run_now_at_ms = EXCLUDED.run_now_at_ms,
647
- created_at_ms = EXCLUDED.created_at_ms,
648
- record = EXCLUDED.record
649
- `,
650
- [
651
- task.id,
652
- task.destination.teamId,
653
- task.status,
654
- task.nextRunAtMs ?? null,
655
- task.runNowAtMs ?? null,
656
- task.createdAtMs,
657
- json(task)
658
- ]
659
- );
684
+ await db.insert(juniorSchedulerTasks).values({
685
+ createdAtMs: task.createdAtMs,
686
+ id: task.id,
687
+ nextRunAtMs: task.nextRunAtMs,
688
+ record: task,
689
+ runNowAtMs: task.runNowAtMs,
690
+ status: task.status,
691
+ teamId: task.destination.teamId
692
+ }).onConflictDoUpdate({
693
+ target: juniorSchedulerTasks.id,
694
+ set: {
695
+ createdAtMs: sql2`excluded.created_at_ms`,
696
+ nextRunAtMs: sql2`excluded.next_run_at_ms`,
697
+ record: sql2`excluded.record`,
698
+ runNowAtMs: sql2`excluded.run_now_at_ms`,
699
+ status: sql2`excluded.status`,
700
+ teamId: sql2`excluded.team_id`
701
+ }
702
+ });
660
703
  }
661
704
  async function upsertSqlRun(db, run) {
662
- await db.execute(
663
- `
664
- INSERT INTO junior_scheduler_runs (
665
- id,
666
- task_id,
667
- status,
668
- scheduled_for_ms,
669
- record
670
- ) VALUES (
671
- $1, $2, $3, $4, $5::jsonb
672
- )
673
- ON CONFLICT (id) DO UPDATE SET
674
- task_id = EXCLUDED.task_id,
675
- status = EXCLUDED.status,
676
- scheduled_for_ms = EXCLUDED.scheduled_for_ms,
677
- record = EXCLUDED.record
678
- `,
679
- [run.id, run.taskId, run.status, run.scheduledForMs, json(run)]
680
- );
705
+ await db.insert(juniorSchedulerRuns).values({
706
+ id: run.id,
707
+ record: run,
708
+ scheduledForMs: run.scheduledForMs,
709
+ status: run.status,
710
+ taskId: run.taskId
711
+ }).onConflictDoUpdate({
712
+ target: juniorSchedulerRuns.id,
713
+ set: {
714
+ record: sql2`excluded.record`,
715
+ scheduledForMs: sql2`excluded.scheduled_for_ms`,
716
+ status: sql2`excluded.status`,
717
+ taskId: sql2`excluded.task_id`
718
+ }
719
+ });
681
720
  }
682
721
  async function getTaskFromSql(db, taskId) {
683
- const rows = await db.query(
684
- "SELECT record FROM junior_scheduler_tasks WHERE id = $1",
685
- [taskId]
686
- );
722
+ const rows = await db.select({ record: juniorSchedulerTasks.record }).from(juniorSchedulerTasks).where(eq(juniorSchedulerTasks.id, taskId)).limit(1);
687
723
  return rows[0] ? parseSqlTaskRow(rows[0]) : void 0;
688
724
  }
689
725
  async function getRunFromSql(db, runId) {
690
- const rows = await db.query(
691
- "SELECT record FROM junior_scheduler_runs WHERE id = $1",
692
- [runId]
693
- );
694
- return rows[0] ? parseSqlRunRow(rows[0]) : void 0;
695
- }
696
- async function getClaimedRunSlotFromSql(db, runId) {
697
- const rows = await db.query(
698
- "SELECT record FROM junior_scheduler_runs WHERE id = $1",
699
- [runId]
700
- );
726
+ const rows = await db.select({ record: juniorSchedulerRuns.record }).from(juniorSchedulerRuns).where(eq(juniorSchedulerRuns.id, runId)).limit(1);
701
727
  return rows[0] ? parseSqlRunRow(rows[0]) : void 0;
702
728
  }
703
729
  async function listTasksFromSql(db) {
704
- const rows = await db.query(
705
- `
706
- SELECT record
707
- FROM junior_scheduler_tasks
708
- WHERE status <> 'deleted'
709
- ORDER BY created_at_ms ASC, id ASC
710
- `
730
+ const rows = await db.select({ record: juniorSchedulerTasks.record }).from(juniorSchedulerTasks).where(ne(juniorSchedulerTasks.status, "deleted")).orderBy(
731
+ asc(juniorSchedulerTasks.createdAtMs),
732
+ asc(juniorSchedulerTasks.id)
711
733
  );
712
734
  return rows.map(parseSqlTaskRow).filter(present);
713
735
  }
714
736
  async function listTasksForTeamFromSql(db, teamId) {
715
- const rows = await db.query(
716
- `
717
- SELECT record
718
- FROM junior_scheduler_tasks
719
- WHERE team_id = $1
720
- AND status <> 'deleted'
721
- ORDER BY created_at_ms ASC, id ASC
722
- `,
723
- [teamId]
737
+ const rows = await db.select({ record: juniorSchedulerTasks.record }).from(juniorSchedulerTasks).where(
738
+ and(
739
+ eq(juniorSchedulerTasks.teamId, teamId),
740
+ ne(juniorSchedulerTasks.status, "deleted")
741
+ )
742
+ ).orderBy(
743
+ asc(juniorSchedulerTasks.createdAtMs),
744
+ asc(juniorSchedulerTasks.id)
724
745
  );
725
746
  return rows.map(parseSqlTaskRow).filter(present);
726
747
  }
@@ -728,15 +749,17 @@ async function listIncompleteRunsForTasksFromSql(db, tasks) {
728
749
  if (tasks.length === 0) {
729
750
  return [];
730
751
  }
731
- const rows = await db.query(
732
- `
733
- SELECT record
734
- FROM junior_scheduler_runs
735
- WHERE task_id = ANY($1)
736
- AND status = ANY($2)
737
- ORDER BY scheduled_for_ms ASC, id ASC
738
- `,
739
- [tasks.map((task) => task.id), [...SQL_INCOMPLETE_RUN_STATUSES]]
752
+ const rows = await db.select({ record: juniorSchedulerRuns.record }).from(juniorSchedulerRuns).where(
753
+ and(
754
+ inArray(
755
+ juniorSchedulerRuns.taskId,
756
+ tasks.map((task) => task.id)
757
+ ),
758
+ inArray(juniorSchedulerRuns.status, [...SQL_INCOMPLETE_RUN_STATUSES])
759
+ )
760
+ ).orderBy(
761
+ asc(juniorSchedulerRuns.scheduledForMs),
762
+ asc(juniorSchedulerRuns.id)
740
763
  );
741
764
  return rows.map(parseSqlRunRow).filter(present);
742
765
  }
@@ -754,9 +777,11 @@ var SqlSchedulerStore = class {
754
777
  }
755
778
  async saveTaskRecord(db, task, current) {
756
779
  if (current?.status === "blocked" && task.status === "active" && typeof task.nextRunAtMs === "number" && Number.isFinite(task.nextRunAtMs)) {
757
- await db.execute(
758
- "DELETE FROM junior_scheduler_runs WHERE id = $1 AND status = 'blocked'",
759
- [buildRunId(task.id, task.nextRunAtMs)]
780
+ await db.delete(juniorSchedulerRuns).where(
781
+ and(
782
+ eq(juniorSchedulerRuns.id, buildRunId(task.id, task.nextRunAtMs)),
783
+ eq(juniorSchedulerRuns.status, "blocked")
784
+ )
760
785
  );
761
786
  }
762
787
  await upsertSqlTask(db, task);
@@ -772,18 +797,23 @@ var SqlSchedulerStore = class {
772
797
  }
773
798
  async claimDueRun(args) {
774
799
  return await withSqlLock(this.db, "junior:scheduler:claim", async (db) => {
775
- const rows = await db.query(
776
- `
777
- SELECT record
778
- FROM junior_scheduler_tasks
779
- WHERE status = 'active'
780
- AND (
781
- (run_now_at_ms IS NOT NULL AND run_now_at_ms <= $1)
782
- OR (next_run_at_ms IS NOT NULL AND next_run_at_ms <= $1)
783
- )
784
- ORDER BY created_at_ms ASC, id ASC
785
- `,
786
- [args.nowMs]
800
+ const rows = await db.select({ record: juniorSchedulerTasks.record }).from(juniorSchedulerTasks).where(
801
+ and(
802
+ eq(juniorSchedulerTasks.status, "active"),
803
+ or(
804
+ and(
805
+ isNotNull(juniorSchedulerTasks.runNowAtMs),
806
+ lte(juniorSchedulerTasks.runNowAtMs, args.nowMs)
807
+ ),
808
+ and(
809
+ isNotNull(juniorSchedulerTasks.nextRunAtMs),
810
+ lte(juniorSchedulerTasks.nextRunAtMs, args.nowMs)
811
+ )
812
+ )
813
+ )
814
+ ).orderBy(
815
+ asc(juniorSchedulerTasks.createdAtMs),
816
+ asc(juniorSchedulerTasks.id)
787
817
  );
788
818
  for (const row of rows) {
789
819
  const task = parseSqlTaskRow(row);
@@ -817,7 +847,7 @@ ORDER BY created_at_ms ASC, id ASC
817
847
  await upsertSqlRun(db, reclaimed);
818
848
  return reclaimed;
819
849
  }
820
- const existingRun = await getClaimedRunSlotFromSql(db, runId);
850
+ const existingRun = await getRunFromSql(db, runId);
821
851
  if (existingRun) {
822
852
  continue;
823
853
  }
@@ -1124,8 +1154,7 @@ import { Type } from "@sinclair/typebox";
1124
1154
  import {
1125
1155
  PluginToolInputError,
1126
1156
  pluginCredentialSubjectSchema as pluginCredentialSubjectSchema2,
1127
- destinationSchema as destinationSchema2,
1128
- isSlackDestination as isSlackDestination2
1157
+ sourceSchema
1129
1158
  } from "@sentry/junior-plugin-api";
1130
1159
 
1131
1160
  // src/types.ts
@@ -1142,7 +1171,7 @@ function throwToolInputError(error) {
1142
1171
  throw new PluginToolInputError(error);
1143
1172
  }
1144
1173
  function requireActiveConversation(context) {
1145
- const parsed = destinationSchema2.safeParse(context.source);
1174
+ const parsed = sourceSchema.safeParse(context.source);
1146
1175
  if (!parsed.success) {
1147
1176
  const source = context.source;
1148
1177
  const issues = parsed.error.issues;
@@ -1162,10 +1191,14 @@ function requireActiveConversation(context) {
1162
1191
  }
1163
1192
  throwToolInputError("No active Slack conversation is available.");
1164
1193
  }
1165
- if (!isSlackDestination2(parsed.data)) {
1194
+ if (parsed.data.platform !== "slack") {
1166
1195
  throwToolInputError("No active Slack conversation is available.");
1167
1196
  }
1168
- return parsed.data;
1197
+ return {
1198
+ platform: "slack",
1199
+ teamId: parsed.data.teamId,
1200
+ channelId: parsed.data.channelId
1201
+ };
1169
1202
  }
1170
1203
  function requireRequester(context) {
1171
1204
  if (context.requester?.platform !== "slack") {
@@ -1653,22 +1686,15 @@ function buildScheduledTaskDispatchMetadata(args) {
1653
1686
  };
1654
1687
  }
1655
1688
  function scheduledTaskDispatchSource(task) {
1656
- return {
1657
- platform: "slack",
1689
+ return createSlackSource({
1658
1690
  teamId: task.destination.teamId,
1659
1691
  channelId: task.destination.channelId
1660
- };
1692
+ });
1661
1693
  }
1662
1694
  function schedulerStore2(ctx) {
1663
- if (!ctx.db) {
1664
- throw new Error("Scheduler plugin requires ctx.db");
1665
- }
1666
1695
  return createSchedulerSqlStore(ctx.db);
1667
1696
  }
1668
1697
  function schedulerOperationalStore(ctx) {
1669
- if (!ctx.db) {
1670
- throw new Error("Scheduler plugin requires ctx.db");
1671
- }
1672
1698
  return createSchedulerOperationalSqlStore(ctx.db);
1673
1699
  }
1674
1700
  function shouldSkipRun(task, run) {
@@ -1686,11 +1712,7 @@ function shouldSkipRun(task, run) {
1686
1712
  function createSchedulerToolContext(ctx) {
1687
1713
  return {
1688
1714
  credentialSubject: ctx.slack?.credentialSubject,
1689
- source: ctx.source.platform === "slack" ? {
1690
- platform: "slack",
1691
- teamId: ctx.source.teamId,
1692
- channelId: ctx.source.channelId
1693
- } : void 0,
1715
+ source: ctx.source.platform === "slack" ? ctx.source : void 0,
1694
1716
  requester: ctx.requester?.platform === "slack" ? ctx.requester : void 0,
1695
1717
  store: schedulerStore2(ctx),
1696
1718
  userText: ctx.userText
@@ -1937,7 +1959,6 @@ async function buildSchedulerOperationalReport(args) {
1937
1959
  }
1938
1960
  function createSchedulerPlugin() {
1939
1961
  return defineJuniorPlugin({
1940
- database: {},
1941
1962
  manifest: {
1942
1963
  name: "scheduler",
1943
1964
  displayName: "Scheduler",
@@ -1985,7 +2006,7 @@ function createSchedulerPlugin() {
1985
2006
  processedCount += 1;
1986
2007
  }
1987
2008
  }
1988
- for (let index = processedCount; index < SCHEDULER_HEARTBEAT_LIMIT; index += 1) {
2009
+ for (let index2 = processedCount; index2 < SCHEDULER_HEARTBEAT_LIMIT; index2 += 1) {
1989
2010
  const run = await store.claimDueRun({ nowMs: ctx.nowMs });
1990
2011
  if (!run) {
1991
2012
  break;
@@ -2062,9 +2083,6 @@ function createSchedulerPlugin() {
2062
2083
  });
2063
2084
  },
2064
2085
  async migrateStorage(ctx) {
2065
- if (!ctx.db) {
2066
- throw new Error("Scheduler storage migration requires ctx.db");
2067
- }
2068
2086
  return await migrateSchedulerStateToSql({
2069
2087
  db: ctx.db,
2070
2088
  state: ctx.state
package/dist/plugin.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  /** Create Junior's built-in trusted scheduler plugin. */
2
2
  export declare function createSchedulerPlugin(): import("@sentry/junior-plugin-api").PluginRegistration;
3
- /** Register trusted scheduler runtime hooks for scheduled Junior tasks. */
3
+ /** Register scheduler runtime hooks for scheduled Junior tasks. */
4
4
  export declare const schedulerPlugin: typeof createSchedulerPlugin;
@@ -1,9 +1,9 @@
1
- import { type PluginCredentialSubject, type PluginToolDefinition, type SlackDestination, type SlackRequester } from "@sentry/junior-plugin-api";
1
+ import { type PluginCredentialSubject, type PluginToolDefinition, type SlackRequester, type SlackSource } from "@sentry/junior-plugin-api";
2
2
  import { type SchedulerStore } from "./store";
3
3
  export interface SchedulerToolContext {
4
4
  credentialSubject?: PluginCredentialSubject;
5
5
  requester?: SlackRequester;
6
- source?: SlackDestination;
6
+ source?: SlackSource;
7
7
  store: SchedulerStore;
8
8
  userText?: string;
9
9
  }
package/dist/store.d.ts CHANGED
@@ -1,5 +1,9 @@
1
- import { type PluginDb, type PluginReadState, type PluginState } from "@sentry/junior-plugin-api";
1
+ import { type PluginReadState, type PluginState } from "@sentry/junior-plugin-api";
2
+ import type { PgDatabase } from "drizzle-orm/pg-core";
3
+ import type { PgQueryResultHKT } from "drizzle-orm/pg-core/session";
4
+ import * as schedulerSqlSchema from "./db/schema";
2
5
  import type { ScheduledRun, ScheduledTask } from "./types";
6
+ export type SchedulerDb = PgDatabase<PgQueryResultHKT, typeof schedulerSqlSchema>;
3
7
  export interface SchedulerStore {
4
8
  claimDueRun(args: {
5
9
  nowMs: number;
@@ -55,12 +59,12 @@ export declare function createSchedulerStore(state: PluginState): SchedulerStore
55
59
  /** Create a read-only scheduler store for operational reporting. */
56
60
  export declare function createSchedulerOperationalStore(state: PluginReadState): SchedulerOperationalStore;
57
61
  /** Create a scheduler store backed by the plugin SQL database. */
58
- export declare function createSchedulerSqlStore(db: PluginDb): SchedulerStore;
62
+ export declare function createSchedulerSqlStore(db: SchedulerDb): SchedulerStore;
59
63
  /** Create a read-only scheduler operational store backed by SQL. */
60
- export declare function createSchedulerOperationalSqlStore(db: PluginDb): SchedulerOperationalStore;
64
+ export declare function createSchedulerOperationalSqlStore(db: SchedulerDb): SchedulerOperationalStore;
61
65
  /** Copy retained scheduler plugin-state records into the scheduler SQL tables. */
62
66
  export declare function migrateSchedulerStateToSql(args: {
63
- db: PluginDb;
67
+ db: SchedulerDb;
64
68
  state: PluginState;
65
69
  }): Promise<{
66
70
  existing: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-scheduler",
3
- "version": "0.75.0",
3
+ "version": "0.76.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -26,7 +26,7 @@
26
26
  "@sinclair/typebox": "^0.34.49",
27
27
  "drizzle-orm": "^0.45.2",
28
28
  "zod": "^4.4.3",
29
- "@sentry/junior-plugin-api": "0.75.0"
29
+ "@sentry/junior-plugin-api": "0.76.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^25.9.1",
package/src/index.ts CHANGED
@@ -11,6 +11,7 @@ export {
11
11
  createSchedulerOperationalSqlStore,
12
12
  createSchedulerSqlStore,
13
13
  migrateSchedulerStateToSql,
14
+ type SchedulerDb,
14
15
  } from "./store";
15
16
  export type {
16
17
  ScheduledCalendarFrequency,
package/src/plugin.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
+ createSlackSource,
2
3
  defineJuniorPlugin,
3
4
  type Dispatch,
4
- type PluginDb,
5
5
  type PluginToolDefinition,
6
6
  type PluginOperationalReportContent,
7
7
  type PluginReadState,
@@ -14,6 +14,7 @@ import {
14
14
  createSchedulerOperationalSqlStore,
15
15
  createSchedulerSqlStore,
16
16
  migrateSchedulerStateToSql,
17
+ type SchedulerDb,
17
18
  type SchedulerOperationalStore,
18
19
  type SchedulerStore,
19
20
  } from "./store";
@@ -76,27 +77,20 @@ function buildScheduledTaskDispatchMetadata(args: {
76
77
  }
77
78
 
78
79
  function scheduledTaskDispatchSource(task: ScheduledTask): Source {
79
- return {
80
- platform: "slack",
80
+ return createSlackSource({
81
81
  teamId: task.destination.teamId,
82
82
  channelId: task.destination.channelId,
83
- };
83
+ });
84
84
  }
85
85
 
86
- function schedulerStore(ctx: { db?: PluginDb }): SchedulerStore {
87
- if (!ctx.db) {
88
- throw new Error("Scheduler plugin requires ctx.db");
89
- }
90
- return createSchedulerSqlStore(ctx.db);
86
+ function schedulerStore(ctx: { db: unknown }): SchedulerStore {
87
+ return createSchedulerSqlStore(ctx.db as SchedulerDb);
91
88
  }
92
89
 
93
90
  function schedulerOperationalStore(ctx: {
94
- db?: PluginDb;
91
+ db: unknown;
95
92
  }): SchedulerOperationalStore {
96
- if (!ctx.db) {
97
- throw new Error("Scheduler plugin requires ctx.db");
98
- }
99
- return createSchedulerOperationalSqlStore(ctx.db);
93
+ return createSchedulerOperationalSqlStore(ctx.db as SchedulerDb);
100
94
  }
101
95
 
102
96
  function shouldSkipRun(
@@ -123,14 +117,7 @@ function createSchedulerToolContext(
123
117
  ): SchedulerToolContext {
124
118
  return {
125
119
  credentialSubject: ctx.slack?.credentialSubject,
126
- source:
127
- ctx.source.platform === "slack"
128
- ? {
129
- platform: "slack",
130
- teamId: ctx.source.teamId,
131
- channelId: ctx.source.channelId,
132
- }
133
- : undefined,
120
+ source: ctx.source.platform === "slack" ? ctx.source : undefined,
134
121
  requester: ctx.requester?.platform === "slack" ? ctx.requester : undefined,
135
122
  store: schedulerStore(ctx),
136
123
  userText: ctx.userText,
@@ -430,7 +417,6 @@ async function buildSchedulerOperationalReport(args: {
430
417
  /** Create Junior's built-in trusted scheduler plugin. */
431
418
  export function createSchedulerPlugin() {
432
419
  return defineJuniorPlugin({
433
- database: {},
434
420
  manifest: {
435
421
  name: "scheduler",
436
422
  displayName: "Scheduler",
@@ -575,11 +561,8 @@ export function createSchedulerPlugin() {
575
561
  });
576
562
  },
577
563
  async migrateStorage(ctx) {
578
- if (!ctx.db) {
579
- throw new Error("Scheduler storage migration requires ctx.db");
580
- }
581
564
  return await migrateSchedulerStateToSql({
582
- db: ctx.db,
565
+ db: ctx.db as SchedulerDb,
583
566
  state: ctx.state,
584
567
  });
585
568
  },
@@ -587,5 +570,5 @@ export function createSchedulerPlugin() {
587
570
  });
588
571
  }
589
572
 
590
- /** Register trusted scheduler runtime hooks for scheduled Junior tasks. */
573
+ /** Register scheduler runtime hooks for scheduled Junior tasks. */
591
574
  export const schedulerPlugin = createSchedulerPlugin;
@@ -3,12 +3,12 @@ import { Type } from "@sinclair/typebox";
3
3
  import {
4
4
  PluginToolInputError,
5
5
  pluginCredentialSubjectSchema,
6
- destinationSchema,
7
- isSlackDestination,
6
+ sourceSchema,
8
7
  type PluginCredentialSubject,
9
8
  type PluginToolDefinition,
10
9
  type SlackDestination,
11
10
  type SlackRequester,
11
+ type SlackSource,
12
12
  } from "@sentry/junior-plugin-api";
13
13
  import { buildCalendarRecurrence, parseScheduleTimestamp } from "./cadence";
14
14
  import { sanitizeScheduledTaskPrincipal } from "./identity";
@@ -26,7 +26,7 @@ import type {
26
26
  export interface SchedulerToolContext {
27
27
  credentialSubject?: PluginCredentialSubject;
28
28
  requester?: SlackRequester;
29
- source?: SlackDestination;
29
+ source?: SlackSource;
30
30
  store: SchedulerStore;
31
31
  userText?: string;
32
32
  }
@@ -47,9 +47,9 @@ function throwToolInputError(error: string): never {
47
47
  function requireActiveConversation(
48
48
  context: SchedulerToolContext,
49
49
  ): SlackDestination {
50
- const parsed = destinationSchema.safeParse(context.source);
50
+ const parsed = sourceSchema.safeParse(context.source);
51
51
  if (!parsed.success) {
52
- const source = context.source as Partial<SlackDestination> | undefined;
52
+ const source = context.source as Partial<SlackSource> | undefined;
53
53
  const issues = parsed.error.issues as readonly SchemaIssue[];
54
54
  if (!source || source.platform !== "slack") {
55
55
  throwToolInputError("No active Slack conversation is available.");
@@ -68,11 +68,15 @@ function requireActiveConversation(
68
68
  throwToolInputError("No active Slack conversation is available.");
69
69
  }
70
70
 
71
- if (!isSlackDestination(parsed.data)) {
71
+ if (parsed.data.platform !== "slack") {
72
72
  throwToolInputError("No active Slack conversation is available.");
73
73
  }
74
74
 
75
- return parsed.data;
75
+ return {
76
+ platform: "slack",
77
+ teamId: parsed.data.teamId,
78
+ channelId: parsed.data.channelId,
79
+ };
76
80
  }
77
81
 
78
82
  function requireRequester(
package/src/store.ts CHANGED
@@ -2,12 +2,26 @@ import {
2
2
  pluginCredentialSubjectSchema,
3
3
  destinationSchema,
4
4
  isSlackDestination,
5
- type PluginDb,
6
5
  type PluginReadState,
7
6
  type PluginState,
8
7
  } from "@sentry/junior-plugin-api";
8
+ import {
9
+ and,
10
+ asc,
11
+ eq,
12
+ inArray,
13
+ isNotNull,
14
+ lte,
15
+ ne,
16
+ or,
17
+ sql,
18
+ } from "drizzle-orm";
19
+ import type { PgDatabase } from "drizzle-orm/pg-core";
20
+ import type { PgQueryResultHKT } from "drizzle-orm/pg-core/session";
9
21
  import { z } from "zod";
10
22
  import { getNextRunAtMs } from "./cadence";
23
+ import * as schedulerSqlSchema from "./db/schema";
24
+ import { juniorSchedulerRuns, juniorSchedulerTasks } from "./db/schema";
11
25
  import type { ScheduledRun, ScheduledTask } from "./types";
12
26
 
13
27
  const SCHEDULER_KEY_PREFIX = "junior:scheduler";
@@ -18,6 +32,10 @@ const PENDING_CLAIM_STALE_MS = 60_000;
18
32
  const MISSED_RUN_MAX_AGE_MS = 24 * 60 * 60 * 1000;
19
33
  const LOCK_TTL_MS = 10_000;
20
34
  const SQL_INCOMPLETE_RUN_STATUSES = ["pending", "running"] as const;
35
+ export type SchedulerDb = PgDatabase<
36
+ PgQueryResultHKT,
37
+ typeof schedulerSqlSchema
38
+ >;
21
39
  const slackDestinationSchema = destinationSchema.refine(isSlackDestination);
22
40
  const taskPrincipalSchema = z
23
41
  .object({
@@ -1055,161 +1073,150 @@ function parseSqlRunRow(row: SchedulerRunRow): ScheduledRun | undefined {
1055
1073
  return parsed.success ? stripLegacyRunFields(parsed.data) : undefined;
1056
1074
  }
1057
1075
 
1058
- function json(value: unknown): string {
1059
- return JSON.stringify(value);
1060
- }
1061
-
1062
1076
  async function withSqlLock<T>(
1063
- db: PluginDb,
1077
+ db: SchedulerDb,
1064
1078
  key: string,
1065
- callback: (db: PluginDb) => Promise<T>,
1079
+ callback: (db: SchedulerDb) => Promise<T>,
1066
1080
  ): Promise<T> {
1067
1081
  return await db.transaction(async (tx) => {
1068
- await tx.execute("SELECT pg_advisory_xact_lock(hashtext($1))", [key]);
1082
+ await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtext(${key}))`);
1069
1083
  return await callback(tx);
1070
1084
  });
1071
1085
  }
1072
1086
 
1073
- async function upsertSqlTask(db: PluginDb, task: ScheduledTask): Promise<void> {
1074
- await db.execute(
1075
- `
1076
- INSERT INTO junior_scheduler_tasks (
1077
- id,
1078
- team_id,
1079
- status,
1080
- next_run_at_ms,
1081
- run_now_at_ms,
1082
- created_at_ms,
1083
- record
1084
- ) VALUES (
1085
- $1, $2, $3, $4, $5, $6, $7::jsonb
1086
- )
1087
- ON CONFLICT (id) DO UPDATE SET
1088
- team_id = EXCLUDED.team_id,
1089
- status = EXCLUDED.status,
1090
- next_run_at_ms = EXCLUDED.next_run_at_ms,
1091
- run_now_at_ms = EXCLUDED.run_now_at_ms,
1092
- created_at_ms = EXCLUDED.created_at_ms,
1093
- record = EXCLUDED.record
1094
- `,
1095
- [
1096
- task.id,
1097
- task.destination.teamId,
1098
- task.status,
1099
- task.nextRunAtMs ?? null,
1100
- task.runNowAtMs ?? null,
1101
- task.createdAtMs,
1102
- json(task),
1103
- ],
1104
- );
1087
+ async function upsertSqlTask(
1088
+ db: SchedulerDb,
1089
+ task: ScheduledTask,
1090
+ ): Promise<void> {
1091
+ await db
1092
+ .insert(juniorSchedulerTasks)
1093
+ .values({
1094
+ createdAtMs: task.createdAtMs,
1095
+ id: task.id,
1096
+ nextRunAtMs: task.nextRunAtMs,
1097
+ record: task,
1098
+ runNowAtMs: task.runNowAtMs,
1099
+ status: task.status,
1100
+ teamId: task.destination.teamId,
1101
+ })
1102
+ .onConflictDoUpdate({
1103
+ target: juniorSchedulerTasks.id,
1104
+ set: {
1105
+ createdAtMs: sql`excluded.created_at_ms`,
1106
+ nextRunAtMs: sql`excluded.next_run_at_ms`,
1107
+ record: sql`excluded.record`,
1108
+ runNowAtMs: sql`excluded.run_now_at_ms`,
1109
+ status: sql`excluded.status`,
1110
+ teamId: sql`excluded.team_id`,
1111
+ },
1112
+ });
1105
1113
  }
1106
1114
 
1107
- async function upsertSqlRun(db: PluginDb, run: ScheduledRun): Promise<void> {
1108
- await db.execute(
1109
- `
1110
- INSERT INTO junior_scheduler_runs (
1111
- id,
1112
- task_id,
1113
- status,
1114
- scheduled_for_ms,
1115
- record
1116
- ) VALUES (
1117
- $1, $2, $3, $4, $5::jsonb
1118
- )
1119
- ON CONFLICT (id) DO UPDATE SET
1120
- task_id = EXCLUDED.task_id,
1121
- status = EXCLUDED.status,
1122
- scheduled_for_ms = EXCLUDED.scheduled_for_ms,
1123
- record = EXCLUDED.record
1124
- `,
1125
- [run.id, run.taskId, run.status, run.scheduledForMs, json(run)],
1126
- );
1115
+ async function upsertSqlRun(db: SchedulerDb, run: ScheduledRun): Promise<void> {
1116
+ await db
1117
+ .insert(juniorSchedulerRuns)
1118
+ .values({
1119
+ id: run.id,
1120
+ record: run,
1121
+ scheduledForMs: run.scheduledForMs,
1122
+ status: run.status,
1123
+ taskId: run.taskId,
1124
+ })
1125
+ .onConflictDoUpdate({
1126
+ target: juniorSchedulerRuns.id,
1127
+ set: {
1128
+ record: sql`excluded.record`,
1129
+ scheduledForMs: sql`excluded.scheduled_for_ms`,
1130
+ status: sql`excluded.status`,
1131
+ taskId: sql`excluded.task_id`,
1132
+ },
1133
+ });
1127
1134
  }
1128
1135
 
1129
1136
  async function getTaskFromSql(
1130
- db: PluginDb,
1137
+ db: SchedulerDb,
1131
1138
  taskId: string,
1132
1139
  ): Promise<ScheduledTask | undefined> {
1133
- const rows = await db.query<SchedulerTaskRow>(
1134
- "SELECT record FROM junior_scheduler_tasks WHERE id = $1",
1135
- [taskId],
1136
- );
1140
+ const rows = await db
1141
+ .select({ record: juniorSchedulerTasks.record })
1142
+ .from(juniorSchedulerTasks)
1143
+ .where(eq(juniorSchedulerTasks.id, taskId))
1144
+ .limit(1);
1137
1145
  return rows[0] ? parseSqlTaskRow(rows[0]) : undefined;
1138
1146
  }
1139
1147
 
1140
1148
  async function getRunFromSql(
1141
- db: PluginDb,
1142
- runId: string,
1143
- ): Promise<ScheduledRun | undefined> {
1144
- const rows = await db.query<SchedulerRunRow>(
1145
- "SELECT record FROM junior_scheduler_runs WHERE id = $1",
1146
- [runId],
1147
- );
1148
- return rows[0] ? parseSqlRunRow(rows[0]) : undefined;
1149
- }
1150
-
1151
- async function getClaimedRunSlotFromSql(
1152
- db: PluginDb,
1149
+ db: SchedulerDb,
1153
1150
  runId: string,
1154
1151
  ): Promise<ScheduledRun | undefined> {
1155
- const rows = await db.query<SchedulerRunRow>(
1156
- "SELECT record FROM junior_scheduler_runs WHERE id = $1",
1157
- [runId],
1158
- );
1152
+ const rows = await db
1153
+ .select({ record: juniorSchedulerRuns.record })
1154
+ .from(juniorSchedulerRuns)
1155
+ .where(eq(juniorSchedulerRuns.id, runId))
1156
+ .limit(1);
1159
1157
  return rows[0] ? parseSqlRunRow(rows[0]) : undefined;
1160
1158
  }
1161
1159
 
1162
- async function listTasksFromSql(db: PluginDb): Promise<ScheduledTask[]> {
1163
- const rows = await db.query<SchedulerTaskRow>(
1164
- `
1165
- SELECT record
1166
- FROM junior_scheduler_tasks
1167
- WHERE status <> 'deleted'
1168
- ORDER BY created_at_ms ASC, id ASC
1169
- `,
1170
- );
1160
+ async function listTasksFromSql(db: SchedulerDb): Promise<ScheduledTask[]> {
1161
+ const rows = await db
1162
+ .select({ record: juniorSchedulerTasks.record })
1163
+ .from(juniorSchedulerTasks)
1164
+ .where(ne(juniorSchedulerTasks.status, "deleted"))
1165
+ .orderBy(
1166
+ asc(juniorSchedulerTasks.createdAtMs),
1167
+ asc(juniorSchedulerTasks.id),
1168
+ );
1171
1169
  return rows.map(parseSqlTaskRow).filter(present);
1172
1170
  }
1173
1171
 
1174
1172
  async function listTasksForTeamFromSql(
1175
- db: PluginDb,
1173
+ db: SchedulerDb,
1176
1174
  teamId: string,
1177
1175
  ): Promise<ScheduledTask[]> {
1178
- const rows = await db.query<SchedulerTaskRow>(
1179
- `
1180
- SELECT record
1181
- FROM junior_scheduler_tasks
1182
- WHERE team_id = $1
1183
- AND status <> 'deleted'
1184
- ORDER BY created_at_ms ASC, id ASC
1185
- `,
1186
- [teamId],
1187
- );
1176
+ const rows = await db
1177
+ .select({ record: juniorSchedulerTasks.record })
1178
+ .from(juniorSchedulerTasks)
1179
+ .where(
1180
+ and(
1181
+ eq(juniorSchedulerTasks.teamId, teamId),
1182
+ ne(juniorSchedulerTasks.status, "deleted"),
1183
+ ),
1184
+ )
1185
+ .orderBy(
1186
+ asc(juniorSchedulerTasks.createdAtMs),
1187
+ asc(juniorSchedulerTasks.id),
1188
+ );
1188
1189
  return rows.map(parseSqlTaskRow).filter(present);
1189
1190
  }
1190
1191
 
1191
1192
  async function listIncompleteRunsForTasksFromSql(
1192
- db: PluginDb,
1193
+ db: SchedulerDb,
1193
1194
  tasks: ScheduledTask[],
1194
1195
  ): Promise<ScheduledRun[]> {
1195
1196
  if (tasks.length === 0) {
1196
1197
  return [];
1197
1198
  }
1198
- const rows = await db.query<SchedulerRunRow>(
1199
- `
1200
- SELECT record
1201
- FROM junior_scheduler_runs
1202
- WHERE task_id = ANY($1)
1203
- AND status = ANY($2)
1204
- ORDER BY scheduled_for_ms ASC, id ASC
1205
- `,
1206
- [tasks.map((task) => task.id), [...SQL_INCOMPLETE_RUN_STATUSES]],
1207
- );
1199
+ const rows = await db
1200
+ .select({ record: juniorSchedulerRuns.record })
1201
+ .from(juniorSchedulerRuns)
1202
+ .where(
1203
+ and(
1204
+ inArray(
1205
+ juniorSchedulerRuns.taskId,
1206
+ tasks.map((task) => task.id),
1207
+ ),
1208
+ inArray(juniorSchedulerRuns.status, [...SQL_INCOMPLETE_RUN_STATUSES]),
1209
+ ),
1210
+ )
1211
+ .orderBy(
1212
+ asc(juniorSchedulerRuns.scheduledForMs),
1213
+ asc(juniorSchedulerRuns.id),
1214
+ );
1208
1215
  return rows.map(parseSqlRunRow).filter(present);
1209
1216
  }
1210
1217
 
1211
1218
  class SqlSchedulerStore implements SchedulerStore, SchedulerOperationalStore {
1212
- constructor(private readonly db: PluginDb) {}
1219
+ constructor(private readonly db: SchedulerDb) {}
1213
1220
 
1214
1221
  async saveTask(task: ScheduledTask): Promise<void> {
1215
1222
  const next = requireStoredTask(task);
@@ -1220,7 +1227,7 @@ class SqlSchedulerStore implements SchedulerStore, SchedulerOperationalStore {
1220
1227
  }
1221
1228
 
1222
1229
  private async saveTaskRecord(
1223
- db: PluginDb,
1230
+ db: SchedulerDb,
1224
1231
  task: ScheduledTask,
1225
1232
  current: ScheduledTask | undefined,
1226
1233
  ): Promise<void> {
@@ -1232,10 +1239,14 @@ class SqlSchedulerStore implements SchedulerStore, SchedulerOperationalStore {
1232
1239
  typeof task.nextRunAtMs === "number" &&
1233
1240
  Number.isFinite(task.nextRunAtMs)
1234
1241
  ) {
1235
- await db.execute(
1236
- "DELETE FROM junior_scheduler_runs WHERE id = $1 AND status = 'blocked'",
1237
- [buildRunId(task.id, task.nextRunAtMs)],
1238
- );
1242
+ await db
1243
+ .delete(juniorSchedulerRuns)
1244
+ .where(
1245
+ and(
1246
+ eq(juniorSchedulerRuns.id, buildRunId(task.id, task.nextRunAtMs)),
1247
+ eq(juniorSchedulerRuns.status, "blocked"),
1248
+ ),
1249
+ );
1239
1250
  }
1240
1251
  await upsertSqlTask(db, task);
1241
1252
  }
@@ -1256,19 +1267,28 @@ class SqlSchedulerStore implements SchedulerStore, SchedulerOperationalStore {
1256
1267
  nowMs: number;
1257
1268
  }): Promise<ScheduledRun | undefined> {
1258
1269
  return await withSqlLock(this.db, "junior:scheduler:claim", async (db) => {
1259
- const rows = await db.query<SchedulerTaskRow>(
1260
- `
1261
- SELECT record
1262
- FROM junior_scheduler_tasks
1263
- WHERE status = 'active'
1264
- AND (
1265
- (run_now_at_ms IS NOT NULL AND run_now_at_ms <= $1)
1266
- OR (next_run_at_ms IS NOT NULL AND next_run_at_ms <= $1)
1267
- )
1268
- ORDER BY created_at_ms ASC, id ASC
1269
- `,
1270
- [args.nowMs],
1271
- );
1270
+ const rows = await db
1271
+ .select({ record: juniorSchedulerTasks.record })
1272
+ .from(juniorSchedulerTasks)
1273
+ .where(
1274
+ and(
1275
+ eq(juniorSchedulerTasks.status, "active"),
1276
+ or(
1277
+ and(
1278
+ isNotNull(juniorSchedulerTasks.runNowAtMs),
1279
+ lte(juniorSchedulerTasks.runNowAtMs, args.nowMs),
1280
+ ),
1281
+ and(
1282
+ isNotNull(juniorSchedulerTasks.nextRunAtMs),
1283
+ lte(juniorSchedulerTasks.nextRunAtMs, args.nowMs),
1284
+ ),
1285
+ ),
1286
+ ),
1287
+ )
1288
+ .orderBy(
1289
+ asc(juniorSchedulerTasks.createdAtMs),
1290
+ asc(juniorSchedulerTasks.id),
1291
+ );
1272
1292
 
1273
1293
  for (const row of rows) {
1274
1294
  const task = parseSqlTaskRow(row);
@@ -1302,7 +1322,7 @@ ORDER BY created_at_ms ASC, id ASC
1302
1322
  await upsertSqlRun(db, reclaimed);
1303
1323
  return reclaimed;
1304
1324
  }
1305
- const existingRun = await getClaimedRunSlotFromSql(db, runId);
1325
+ const existingRun = await getRunFromSql(db, runId);
1306
1326
  if (existingRun) {
1307
1327
  continue;
1308
1328
  }
@@ -1330,7 +1350,7 @@ ORDER BY created_at_ms ASC, id ASC
1330
1350
  }
1331
1351
 
1332
1352
  private async skipMissedRun(
1333
- db: PluginDb,
1353
+ db: SchedulerDb,
1334
1354
  args: {
1335
1355
  nowMs: number;
1336
1356
  scheduledForMs: number;
@@ -1387,7 +1407,7 @@ ORDER BY created_at_ms ASC, id ASC
1387
1407
  }
1388
1408
 
1389
1409
  private async findStaleRecoveryCanonicalTask(
1390
- db: PluginDb,
1410
+ db: SchedulerDb,
1391
1411
  task: ScheduledTask,
1392
1412
  ): Promise<ScheduledTask | undefined> {
1393
1413
  const fingerprint = taskDedupeFingerprint(task);
@@ -1625,20 +1645,20 @@ ORDER BY created_at_ms ASC, id ASC
1625
1645
  }
1626
1646
 
1627
1647
  /** Create a scheduler store backed by the plugin SQL database. */
1628
- export function createSchedulerSqlStore(db: PluginDb): SchedulerStore {
1648
+ export function createSchedulerSqlStore(db: SchedulerDb): SchedulerStore {
1629
1649
  return new SqlSchedulerStore(db);
1630
1650
  }
1631
1651
 
1632
1652
  /** Create a read-only scheduler operational store backed by SQL. */
1633
1653
  export function createSchedulerOperationalSqlStore(
1634
- db: PluginDb,
1654
+ db: SchedulerDb,
1635
1655
  ): SchedulerOperationalStore {
1636
1656
  return new SqlSchedulerStore(db);
1637
1657
  }
1638
1658
 
1639
1659
  /** Copy retained scheduler plugin-state records into the scheduler SQL tables. */
1640
1660
  export async function migrateSchedulerStateToSql(args: {
1641
- db: PluginDb;
1661
+ db: SchedulerDb;
1642
1662
  state: PluginState;
1643
1663
  }): Promise<{
1644
1664
  existing: number;