@zhin.js/schedule-feature 1.1.3 → 1.1.5

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/README.md CHANGED
@@ -4,14 +4,19 @@ Plugin Runtime 的 generation-owned Schedule Feature。插件在 `schedules/<nam
4
4
 
5
5
  ```ts
6
6
  import { defineSchedule } from '@zhin.js/schedule-feature';
7
+ import { reportServiceToken } from './report-service.js';
7
8
 
8
9
  export default defineSchedule({
9
10
  cron: '0 0 9 * * *',
10
11
  description: 'Daily report',
11
- async execute() {
12
- // work
12
+ async execute(ctx) {
13
+ await ctx.use(reportServiceToken).sendDailyReport();
13
14
  },
14
15
  });
15
16
  ```
16
17
 
17
18
  也可在 `plugin.ts` 的 `setup({ addSchedule })` 中注入同一定义。Feature 在新 generation 激活时通过 owner-scoped `scheduleHostToken` 注册任务,并在旧 generation lease 释放后撤销旧注册。
19
+
20
+ `execute(ctx)` 的 `owner`、`generation`、`config`、`use(token)` 与 `project(feature)` 来自本次任务所属的 generation。目录声明和 `addSchedule` 使用同一接口;旧代停止接纳新执行,已进入的任务通过现有操作 lease 完成。无入站消息的后台任务不提供 Message、sender 或交互。
21
+
22
+ 可以用 `defineSchedule<MyConfig>(definition)` 标注插件配置。跨重启的提醒、取消状态和投递目标应存入 owner 的 PersistentState;不要把 `ctx`、snapshot、Message、Client 或回调写入 cache 文件。cron、重叠、错过执行和投递未知的处理是不同问题,插件应明确业务策略,不能把再次执行等同于安全重试。
@@ -1,9 +1,15 @@
1
- import type { ScheduleJobRegistration } from '@zhin.js/plugin-runtime';
2
- export type ScheduleDefinition = Omit<ScheduleJobRegistration, 'id' | 'owner'>;
1
+ import type { CapabilityContext } from '@zhin.js/feature-kit';
2
+ /** Operation-scoped owner, configuration and resources; never persist this context. */
3
+ export type ScheduleContext<TConfig = unknown> = CapabilityContext<TConfig>;
4
+ export interface ScheduleDefinition<TConfig = unknown> {
5
+ readonly cron: string;
6
+ readonly description?: string;
7
+ execute(context: ScheduleContext<TConfig>): void | Promise<void>;
8
+ }
3
9
  declare module '@zhin.js/plugin-runtime' {
4
- interface PluginSetupContext {
5
- addSchedule(localName: string, definition: ScheduleDefinition): void;
10
+ interface PluginSetupContext<TConfig = unknown> {
11
+ addSchedule(localName: string, definition: ScheduleDefinition<TConfig>): void;
6
12
  }
7
13
  }
8
- export declare function defineSchedule(definition: ScheduleDefinition): Readonly<ScheduleDefinition>;
9
- export declare function parseScheduleDefinition(value: unknown): Readonly<ScheduleDefinition>;
14
+ export declare function defineSchedule<TConfig = unknown>(definition: ScheduleDefinition<TConfig>): Readonly<ScheduleDefinition<TConfig>>;
15
+ export declare function parseScheduleDefinition<TConfig = unknown>(value: unknown): Readonly<ScheduleDefinition<TConfig>>;
package/lib/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { defineSchedule, parseScheduleDefinition } from './definition.js';
2
- export type { ScheduleDefinition } from './definition.js';
2
+ export type { ScheduleDefinition, ScheduleContext } from './definition.js';
3
3
  export { ScheduleIndex } from './schedule-index.js';
4
4
  export { default, scheduleFeatureId } from './provider.js';
package/lib/provider.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import { ScheduleIndex } from './schedule-index.js';
2
2
  export declare const scheduleFeatureId: import("@zhin.js/plugin-runtime").FeatureId;
3
- declare const scheduleFeature: Readonly<import("@zhin.js/feature-kit").FeatureProvider<Readonly<import("./definition.js").ScheduleDefinition>, ScheduleIndex>>;
3
+ declare const scheduleFeature: Readonly<import("@zhin.js/feature-kit").FeatureProvider<Readonly<import("./definition.js").ScheduleDefinition<unknown>>, ScheduleIndex>>;
4
4
  export default scheduleFeature;
package/lib/provider.js CHANGED
@@ -12,7 +12,7 @@ const scheduleFeature = defineFeatureProvider({
12
12
  id: 'schedules-index',
13
13
  layouts: [{ segments: ['schedules', capture('name')], localName: (values) => captured(values, 'name') }],
14
14
  })],
15
- validate: parseScheduleDefinition,
15
+ validate: value => parseScheduleDefinition(value),
16
16
  },
17
17
  runtime: {
18
18
  project(slots, context) {
@@ -1,4 +1,4 @@
1
- import { type CapabilitySlot, type RuntimeSnapshot } from '@zhin.js/plugin-runtime';
1
+ import { generationAdmissionSource, type CapabilitySlot, type RuntimeSnapshot } from '@zhin.js/plugin-runtime';
2
2
  import type { ScheduleDefinition } from './definition.js';
3
3
  export declare class ScheduleIndex {
4
4
  #private;
@@ -8,4 +8,6 @@ export declare class ScheduleIndex {
8
8
  constructor(slots: readonly Readonly<CapabilitySlot<ScheduleDefinition>>[], snapshot: RuntimeSnapshot);
9
9
  start(): void;
10
10
  stop(): void;
11
+ /** @internal Candidate callbacks only enter after their projection is published. */
12
+ get [generationAdmissionSource](): import("@zhin.js/plugin-runtime").GenerationAdmissionGate[];
11
13
  }
@@ -1,9 +1,12 @@
1
- import { scheduleHostToken, } from '@zhin.js/plugin-runtime';
1
+ import { scheduleHostToken, stagePluginScheduleRegistration, createGenerationAdmissionGate, createPluginScheduleCandidate, generationAdmissionSource, scopeSetupFinalizer, } from '@zhin.js/plugin-runtime';
2
+ import { createCapabilityContext } from '@zhin.js/feature-kit';
2
3
  export class ScheduleIndex {
3
4
  slots;
4
5
  snapshot;
5
6
  $projection = 'zhin.schedule-index/1';
6
7
  #disposers = [];
8
+ #admission = createGenerationAdmissionGate();
9
+ #hosts = new Map();
7
10
  #started = false;
8
11
  constructor(slots, snapshot) {
9
12
  this.slots = slots;
@@ -19,22 +22,49 @@ export class ScheduleIndex {
19
22
  const host = resources?.get(scheduleHostToken.id);
20
23
  if (!host)
21
24
  throw new Error(`Schedule Host is unavailable for ${slot.owner}`);
22
- this.#disposers.push(host.register({
25
+ let candidate = this.#hosts.get(slot.owner);
26
+ if (!candidate) {
27
+ candidate = createPluginScheduleCandidate(host, this.#admission);
28
+ this.#hosts.set(slot.owner, candidate);
29
+ }
30
+ this.#disposers.push(stagePluginScheduleRegistration(candidate, {
23
31
  id: slot.localName,
24
32
  cron: slot.definition.cron,
25
33
  description: slot.definition.description,
26
- execute: slot.definition.execute,
34
+ execute: () => slot.definition.execute(createCapabilityContext(this.snapshot, slot.owner, this.#admission)),
27
35
  }));
28
36
  }
29
37
  }
30
38
  catch (error) {
31
- this.stop();
39
+ const cleanupErrors = [];
40
+ try {
41
+ this.stop();
42
+ }
43
+ catch (cleanupError) {
44
+ cleanupErrors.push(cleanupError);
45
+ }
46
+ if (cleanupErrors.length > 0)
47
+ throw new AggregateError([error, ...cleanupErrors], 'Schedule activation and cleanup both failed', { cause: error });
32
48
  throw error;
33
49
  }
34
50
  }
35
51
  stop() {
36
- for (const dispose of this.#disposers.splice(0).reverse())
37
- dispose();
52
+ const errors = [];
53
+ for (const dispose of this.#disposers.splice(0).reverse()) {
54
+ try {
55
+ dispose();
56
+ }
57
+ catch (error) {
58
+ errors.push(error);
59
+ }
60
+ }
61
+ for (const host of this.#hosts.values())
62
+ host[scopeSetupFinalizer]('dispose');
63
+ this.#hosts.clear();
38
64
  this.#started = false;
65
+ if (errors.length > 0)
66
+ throw new AggregateError(errors, 'Schedule cleanup failed');
39
67
  }
68
+ /** @internal Candidate callbacks only enter after their projection is published. */
69
+ get [generationAdmissionSource]() { return [this.#admission]; }
40
70
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/schedule-feature",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "Generation-owned schedule Feature for Zhin Plugin Runtime",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -8,7 +8,6 @@
8
8
  "exports": {
9
9
  ".": {
10
10
  "types": "./lib/index.d.ts",
11
- "development": "./src/index.ts",
12
11
  "import": "./lib/index.js"
13
12
  }
14
13
  },
@@ -17,8 +16,8 @@
17
16
  "src"
18
17
  ],
19
18
  "dependencies": {
20
- "@zhin.js/feature-kit": "1.1.3",
21
- "@zhin.js/plugin-runtime": "1.1.12"
19
+ "@zhin.js/plugin-runtime": "1.1.14",
20
+ "@zhin.js/feature-kit": "1.1.5"
22
21
  },
23
22
  "devDependencies": {
24
23
  "@types/node": "^26.1.2",
@@ -32,7 +31,7 @@
32
31
  "featureApi": "1.0.0"
33
32
  },
34
33
  "engines": {
35
- "node": "^20.19.0 || >=22.12.0"
34
+ "node": ">=22.19.0"
36
35
  },
37
36
  "license": "MIT",
38
37
  "private": false,
package/src/definition.ts CHANGED
@@ -1,22 +1,29 @@
1
- import type { ScheduleJobRegistration } from '@zhin.js/plugin-runtime';
1
+ import type { CapabilityContext } from '@zhin.js/feature-kit';
2
2
 
3
- export type ScheduleDefinition = Omit<ScheduleJobRegistration, 'id' | 'owner'>;
3
+ /** Operation-scoped owner, configuration and resources; never persist this context. */
4
+ export type ScheduleContext<TConfig = unknown> = CapabilityContext<TConfig>;
5
+
6
+ export interface ScheduleDefinition<TConfig = unknown> {
7
+ readonly cron: string;
8
+ readonly description?: string;
9
+ execute(context: ScheduleContext<TConfig>): void | Promise<void>;
10
+ }
4
11
 
5
12
  declare module '@zhin.js/plugin-runtime' {
6
- interface PluginSetupContext {
7
- addSchedule(localName: string, definition: ScheduleDefinition): void;
13
+ interface PluginSetupContext<TConfig = unknown> {
14
+ addSchedule(localName: string, definition: ScheduleDefinition<TConfig>): void;
8
15
  }
9
16
  }
10
17
 
11
- export function defineSchedule(definition: ScheduleDefinition): Readonly<ScheduleDefinition> {
12
- return parseScheduleDefinition(definition);
18
+ export function defineSchedule<TConfig = unknown>(definition: ScheduleDefinition<TConfig>): Readonly<ScheduleDefinition<TConfig>> {
19
+ return parseScheduleDefinition<TConfig>(definition);
13
20
  }
14
21
 
15
- export function parseScheduleDefinition(value: unknown): Readonly<ScheduleDefinition> {
22
+ export function parseScheduleDefinition<TConfig = unknown>(value: unknown): Readonly<ScheduleDefinition<TConfig>> {
16
23
  if (!value || typeof value !== 'object') {
17
24
  throw new TypeError('Schedule definition must be an object');
18
25
  }
19
- const definition = value as Partial<ScheduleDefinition>;
26
+ const definition = value as Partial<ScheduleDefinition<TConfig>>;
20
27
  if (typeof definition.cron !== 'string' || definition.cron.trim() === '') {
21
28
  throw new TypeError('Schedule definition requires a non-empty cron string');
22
29
  }
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { defineSchedule, parseScheduleDefinition } from './definition.js';
2
- export type { ScheduleDefinition } from './definition.js';
2
+ export type { ScheduleDefinition, ScheduleContext } from './definition.js';
3
3
  export { ScheduleIndex } from './schedule-index.js';
4
4
  export { default, scheduleFeatureId } from './provider.js';
package/src/provider.ts CHANGED
@@ -14,7 +14,7 @@ const scheduleFeature = defineFeatureProvider({
14
14
  id: 'schedules-index',
15
15
  layouts: [{ segments: ['schedules', capture('name')], localName: (values) => captured(values, 'name') }],
16
16
  })],
17
- validate: parseScheduleDefinition,
17
+ validate: value => parseScheduleDefinition(value),
18
18
  },
19
19
  runtime: {
20
20
  project(slots, context) {
@@ -1,15 +1,24 @@
1
1
  import {
2
2
  scheduleHostToken,
3
+ stagePluginScheduleRegistration,
4
+ createGenerationAdmissionGate,
5
+ createPluginScheduleCandidate,
6
+ generationAdmissionSource,
7
+ scopeSetupFinalizer,
3
8
  type CapabilitySlot,
4
9
  type Dispose,
5
10
  type PluginScheduleHost,
6
11
  type RuntimeSnapshot,
12
+ type PluginId,
7
13
  } from '@zhin.js/plugin-runtime';
8
14
  import type { ScheduleDefinition } from './definition.js';
15
+ import { createCapabilityContext } from '@zhin.js/feature-kit';
9
16
 
10
17
  export class ScheduleIndex {
11
18
  readonly $projection = 'zhin.schedule-index/1' as const;
12
19
  readonly #disposers: Dispose[] = [];
20
+ readonly #admission = createGenerationAdmissionGate();
21
+ readonly #hosts = new Map<PluginId, PluginScheduleHost>();
13
22
  #started = false;
14
23
 
15
24
  constructor(
@@ -25,21 +34,37 @@ export class ScheduleIndex {
25
34
  const resources = this.snapshot.resources.get(slot.owner);
26
35
  const host = resources?.get(scheduleHostToken.id) as PluginScheduleHost | undefined;
27
36
  if (!host) throw new Error(`Schedule Host is unavailable for ${slot.owner}`);
28
- this.#disposers.push(host.register({
37
+ let candidate = this.#hosts.get(slot.owner);
38
+ if (!candidate) {
39
+ candidate = createPluginScheduleCandidate(host, this.#admission);
40
+ this.#hosts.set(slot.owner, candidate);
41
+ }
42
+ this.#disposers.push(stagePluginScheduleRegistration(candidate, {
29
43
  id: slot.localName,
30
44
  cron: slot.definition.cron,
31
45
  description: slot.definition.description,
32
- execute: slot.definition.execute,
46
+ execute: () => slot.definition.execute(createCapabilityContext(this.snapshot, slot.owner, this.#admission)),
33
47
  }));
34
48
  }
35
49
  } catch (error) {
36
- this.stop();
50
+ const cleanupErrors: unknown[] = [];
51
+ try { this.stop(); } catch (cleanupError) { cleanupErrors.push(cleanupError); }
52
+ if (cleanupErrors.length > 0) throw new AggregateError([error, ...cleanupErrors], 'Schedule activation and cleanup both failed', { cause: error });
37
53
  throw error;
38
54
  }
39
55
  }
40
56
 
41
57
  stop(): void {
42
- for (const dispose of this.#disposers.splice(0).reverse()) dispose();
58
+ const errors: unknown[] = [];
59
+ for (const dispose of this.#disposers.splice(0).reverse()) {
60
+ try { dispose(); } catch (error) { errors.push(error); }
61
+ }
62
+ for (const host of this.#hosts.values()) host[scopeSetupFinalizer]('dispose');
63
+ this.#hosts.clear();
43
64
  this.#started = false;
65
+ if (errors.length > 0) throw new AggregateError(errors, 'Schedule cleanup failed');
44
66
  }
67
+
68
+ /** @internal Candidate callbacks only enter after their projection is published. */
69
+ get [generationAdmissionSource]() { return [this.#admission]; }
45
70
  }