@hile/schedule 3.0.1 → 3.0.2

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.
Files changed (3) hide show
  1. package/AI.md +263 -0
  2. package/README.md +36 -100
  3. package/package.json +6 -5
package/AI.md ADDED
@@ -0,0 +1,263 @@
1
+ # AI Guide For @hile/schedule
2
+
3
+
4
+
5
+ <!-- Generated by scripts/build-ai-context.mjs from docs/ai. Do not edit by hand. -->
6
+
7
+
8
+
9
+ Purpose: Run cron or delayed jobs, optionally in distributed mode with Redis lock protection.
10
+
11
+
12
+
13
+ Use this file when an AI agent installs the npm package and needs package-local examples, package selection rules, boundaries, and verification steps.
14
+
15
+
16
+
17
+ ## Package Selection
18
+
19
+
20
+
21
+ | User asks for | Use | Also read |
22
+ |---|---|---|
23
+ | Run cron or delayed jobs | `@hile/schedule` | `packages/infrastructure.md` |
24
+
25
+
26
+
27
+ # Infrastructure Helpers
28
+
29
+ Packages: `@hile/typeorm`, `@hile/ioredis`, `@hile/logger`, `@hile/cache`, `@hile/schedule`, `@hile/loader`.
30
+
31
+ ## Use When
32
+
33
+ Use these packages for database connections, Redis connections, structured logging, typed Redis caches, scheduled jobs, and file-system loaders.
34
+
35
+ ## Do Not Use When
36
+
37
+ - Do not use default TypeORM or Redis services for multiple connections.
38
+ - Do not use `@hile/cache` without returning `new Cache(...)` from the loader function.
39
+ - Do not use `@hile/schedule` distributed mode without Redis lock TTLs sized for job duration.
40
+
41
+ ## Install
42
+
43
+ ```bash
44
+ pnpm add @hile/typeorm @hile/ioredis @hile/logger @hile/cache @hile/schedule @hile/loader
45
+ ```
46
+
47
+ ## Imports
48
+
49
+ ```ts
50
+ import typeormService, { transaction } from '@hile/typeorm'
51
+ import redisService from '@hile/ioredis'
52
+ import { createLogger } from '@hile/logger'
53
+ import { Cache, defineCache, RedisCache } from '@hile/cache'
54
+ import { Scheduler, defineJob } from '@hile/schedule'
55
+ import { scanDirectory, compileRoutePath, toRouterPath, normalizePath, Loader } from '@hile/loader'
56
+ ```
57
+
58
+ ## Copy-Paste Example
59
+
60
+ ```ts
61
+ import { loadService } from '@hile/core'
62
+ import redisService from '@hile/ioredis'
63
+ import { Cache, defineCache, RedisCache } from '@hile/cache'
64
+
65
+ const userProfile = defineCache('user:{id:string}:profile', async ({ id }) => {
66
+ const user = await loadUser(id)
67
+ return new Cache(user).setExpire(300)
68
+ })
69
+
70
+ const redis = await loadService(redisService)
71
+ const cache = new RedisCache('app:', redis)
72
+ const users = await cache.loadCache(userProfile)
73
+ const user = await users.read({ id: 'u1' })
74
+ ```
75
+
76
+ ## More Examples
77
+
78
+ TypeORM transaction with compensation:
79
+
80
+ ```ts
81
+ await transaction(ds, async (runner, rollback) => {
82
+ const user = await runner.manager.save(User, input)
83
+ rollback(() => redis.del(`user:${user.id}`))
84
+ await runner.manager.save(AuditLog, { userId: user.id, action: 'create' })
85
+ return user
86
+ })
87
+ ```
88
+
89
+ Distributed scheduled job:
90
+
91
+ ```ts
92
+ const scheduler = new Scheduler()
93
+ scheduler.add('daily-report', '0 8 * * *', async () => {
94
+ await sendDailyReport()
95
+ }, {
96
+ distributed: {
97
+ redis,
98
+ ttl: 60_000,
99
+ namespace: 'reports',
100
+ policy: 'skip-if-locked',
101
+ },
102
+ })
103
+ ```
104
+
105
+ ## Compose With
106
+
107
+ - Use `RedisCache` with `@hile/ioredis`.
108
+ - Use `defineCache(..., { singleflight: true })` to reduce stampedes; internally it uses Redis locks.
109
+ - Use scheduler distributed mode with `@hile/redis-lock`.
110
+ - Use `Loader` only when building a new file-system based convention.
111
+
112
+ ## Runtime And Lifecycle Notes
113
+
114
+ - `Cache(undefined)` removes the key unless negative caching is configured.
115
+ - `Cache#setExpire(seconds)` uses seconds.
116
+ - `defineCache` typed placeholders support `string`, `number`, and `boolean`.
117
+ - `RedisCache.loadCache()` returns `read`, `write`, `remove`, `has`, and `multi`.
118
+ - `RedisCache.removeTag(tag)` removes tagged cache entries.
119
+ - `fieldable` caches use Redis hashes and cannot combine with stale or negative cache options.
120
+ - `Scheduler.add()` supports cron strings and `{ delay }`.
121
+ - `Scheduler.load()` reads default exports from `*.schedule.*` files produced by `defineJob()`.
122
+ - `scanDirectory()` matches `.ts`, `.js`, `.tsx`, `.jsx`, and `.mjs`.
123
+
124
+ ## Anti-Patterns
125
+
126
+ - Returning raw values from `defineCache` handlers instead of `new Cache(value)`.
127
+ - Treating cache as source of truth.
128
+ - Forgetting to destroy manually created Redis or TypeORM clients.
129
+ - Scheduling jobs without idempotency when side effects can repeat.
130
+
131
+ ## Verification Checklist
132
+
133
+ - Manual Redis clients call `disconnect()` during cleanup.
134
+ - Manual TypeORM data sources call `destroy()` during cleanup.
135
+ - Cache keys include app/tenant prefixes when shared Redis is used.
136
+ - Scheduled jobs have clear duplicate-run policy.
137
+
138
+
139
+
140
+ # Related Recipes
141
+
142
+
143
+
144
+ # Queue Worker With Idempotency
145
+
146
+ ## Complete Example
147
+
148
+ ```ts
149
+ // src/services/email-worker.boot.ts
150
+ import { defineService, loadService } from '@hile/core'
151
+ import redisService from '@hile/ioredis'
152
+ import { RedisIdempotency, stableHash } from '@hile/redis-idempotency'
153
+ import { RedisStreamQueue, defineQueue } from '@hile/redis-stream-queue'
154
+
155
+ type EmailPayload = {
156
+ tenantId: string
157
+ userId: string
158
+ template: 'welcome'
159
+ }
160
+
161
+ const emailQueue = defineQueue<EmailPayload>('email')
162
+
163
+ export default defineService('email.worker', async (shutdown) => {
164
+ const redis = await loadService(redisService)
165
+ const queue = new RedisStreamQueue(redis, { prefix: 'app:' })
166
+ const idempotency = new RedisIdempotency(redis)
167
+
168
+ const worker = queue.worker(emailQueue, async (job) => {
169
+ await idempotency.run(
170
+ `idem:email:${job.data.tenantId}:${job.jobId ?? job.id}`,
171
+ () => sendEmail(job.data),
172
+ {
173
+ lockTtl: 60_000,
174
+ resultTtl: 86_400_000,
175
+ fingerprint: stableHash(job.data),
176
+ },
177
+ )
178
+ }, {
179
+ group: 'email-workers',
180
+ consumer: process.env.HOSTNAME ?? `${process.pid}`,
181
+ concurrency: 8,
182
+ claimIdle: 60_000,
183
+ })
184
+
185
+ worker.start()
186
+ shutdown(() => worker.stop())
187
+
188
+ return { queue, worker }
189
+ })
190
+ ```
191
+
192
+ Enqueue:
193
+
194
+ ```ts
195
+ await queue.add(emailQueue, {
196
+ tenantId: 't1',
197
+ userId: 'u1',
198
+ template: 'welcome',
199
+ }, {
200
+ jobId: 'welcome:t1:u1',
201
+ maxAttempts: 5,
202
+ backoff: { type: 'exponential', baseMs: 1_000, maxMs: 60_000 },
203
+ })
204
+ ```
205
+
206
+ ## File Layout
207
+
208
+ ```text
209
+ src/
210
+ services/email-worker.boot.ts
211
+ queues/email.queue.ts
212
+ ```
213
+
214
+ ## User Intent
215
+
216
+ Use this recipe for at-least-once background work where side effects must survive retries safely.
217
+
218
+ ## Packages To Use
219
+
220
+ - `@hile/redis-stream-queue`
221
+ - `@hile/redis-idempotency`
222
+ - `@hile/ioredis`
223
+ - `@hile/core`
224
+
225
+ ## Implementation Steps
226
+
227
+ 1. Define the queue and payload type.
228
+ 2. Enqueue with a stable `jobId`.
229
+ 3. Wrap side effects with `RedisIdempotency.run()`.
230
+ 4. Use business identifiers in idempotency keys.
231
+ 5. Monitor DLQ with `readDeadLetters()`.
232
+
233
+ ## Failure And Cleanup Behavior
234
+
235
+ - Queue delivery is at-least-once.
236
+ - Failed attempts retry until `maxAttempts`, then move to DLQ.
237
+ - `jobId` dedupes enqueue, not side effects.
238
+ - Idempotency caches successful results but is not exactly-once.
239
+
240
+ ## Verification Checklist
241
+
242
+ - Handler is idempotent.
243
+ - Idempotency key is business-derived.
244
+ - Worker stop is registered with `shutdown`.
245
+ - DLQ read path exists.
246
+
247
+
248
+
249
+ # Global Guardrails
250
+
251
+
252
+
253
+ ## Never Generate These Patterns
254
+
255
+ - Do not call `loadService()` at module top level; it starts resources during import.
256
+ - Do not default-export plain functions from `*.boot.*` files; `hile start` expects a Hile service.
257
+ - Do not set `ctx.body` and also return a controller value.
258
+ - Do not assume `@hile/http` Zod validation mutates or coerces `ctx.query`, `ctx.params`, or `ctx.request.body`.
259
+ - Do not put reusable business logic only in controllers, pages, queue workers, or message handlers.
260
+ - Do not use old message examples that append a secondary response getter; current request APIs return promises directly.
261
+ - Do not claim exactly-once delivery or execution from Redis locks, queues, idempotency, or rate limits.
262
+ - Do not use queue `jobId` as the only side-effect idempotency boundary.
263
+ - Do not log the entire async context by default.
package/README.md CHANGED
@@ -1,123 +1,59 @@
1
1
  # @hile/schedule
2
2
 
3
- Declarative job scheduler based on [node-schedule](https://github.com/node-schedule/node-schedule).
3
+ <!-- Generated by scripts/build-ai-context.mjs from docs/ai. Do not edit by hand. -->
4
4
 
5
- 3.0.0 开始,新增或重构的 Hile 架构包统一进入 3.x 版本线,2.x 时代结束。
5
+ Run cron or delayed jobs, optionally in distributed mode with Redis lock protection.
6
6
 
7
- ## Usage
7
+ This README is intentionally short and example-first. The complete AI-facing guide ships in `AI.md` in this package.
8
8
 
9
- ### Code-defined jobs
9
+ ## When To Use
10
10
 
11
- ```ts
12
- import { Scheduler, defineJob } from '@hile/schedule'
13
- import { second } from '@hile/schedule'
14
-
15
- const scheduler = new Scheduler()
16
-
17
- // Cron expression
18
- scheduler.add('daily-report', '0 8 * * *', () => {
19
- console.log('generating daily report...')
20
- })
11
+ Use these packages for database connections, Redis connections, structured logging, typed Redis caches, scheduled jobs, and file-system loaders.
21
12
 
22
- // Delay (毫秒)
23
- scheduler.add('delayed-task', { delay: 5000 }, () => {
24
- console.log('ran after 5 seconds')
25
- })
13
+ ## Install
26
14
 
27
- scheduler.stop() // cancel all jobs
15
+ ```bash
16
+ pnpm add @hile/schedule
28
17
  ```
29
18
 
30
- ### Distributed jobs
31
-
32
- `@hile/schedule` can use `@hile/redis-lock` to make a job run on only one process when several app instances register the same schedule.
19
+ ## Copy-Paste Example
33
20
 
34
21
  ```ts
35
- const scheduler = new Scheduler()
36
-
37
- scheduler.add('daily-report', '0 8 * * *', async () => {
38
- await generateDailyReport()
39
- }, {
40
- distributed: {
41
- redis,
42
- ttl: 60_000,
43
- },
44
- onSkip(info) {
45
- console.log('job skipped because another instance owns the lock', info.id)
46
- },
47
- })
48
- ```
22
+ import { loadService } from '@hile/core'
23
+ import redisService from '@hile/ioredis'
24
+ import { Cache, defineCache, RedisCache } from '@hile/cache'
49
25
 
50
- The default policy is `skip-if-locked`: if another process owns `schedule:{namespace}:{jobId}`, this run is skipped. Without `namespace`, the default namespace is `default`. Use `policy: 'wait'` with `wait` when a delayed wait is acceptable.
51
- Set `distributed.namespace` when several apps share the same Redis but can run jobs with the same id independently.
52
-
53
- ### Auto-load from directory
54
-
55
- Create files with `{name}.schedule.ts`:
56
-
57
- ```ts
58
- // tasks/daily-report.schedule.ts
59
- import { defineJob } from '@hile/schedule'
60
-
61
- export default defineJob('daily-report', '0 8 * * *', () => {
62
- console.log('daily report generated')
63
- }, {
64
- distributed: { redis, ttl: 60_000 },
26
+ const userProfile = defineCache('user:{id:string}:profile', async ({ id }) => {
27
+ const user = await loadUser(id)
28
+ return new Cache(user).setExpire(300)
65
29
  })
66
- ```
67
-
68
- Load them:
69
30
 
70
- ```ts
71
- const scheduler = new Scheduler()
72
- const off = await scheduler.load(join(__dirname, 'tasks'))
73
- // off() to unregister all loaded jobs
31
+ const redis = await loadService(redisService)
32
+ const cache = new RedisCache('app:', redis)
33
+ const users = await cache.loadCache(userProfile)
34
+ const user = await users.read({ id: 'u1' })
74
35
  ```
75
36
 
76
- Custom suffix via `suffix` option:
77
-
78
- ```ts
79
- await scheduler.load('./jobs', { suffix: 'job' })
80
- // loads *.job.ts, *.job.js, etc.
81
- ```
82
-
83
- ### API
84
-
85
- #### `defineJob(id, expression, handler, options?)`
86
-
87
- - `id: string | number` — stable task id. Recommended for distributed jobs and logs.
88
- - `expression: string | { delay: number }` — cron 表达式或延迟毫秒数
89
- - Returns `{ id, type: 'job', expression, handler, options }`
90
-
91
- #### `defineJob(expression, handler, options?)`
92
-
93
- - `expression: string | { delay: number }` — cron 表达式或延迟毫秒数
94
- - Returns `{ id: number, idAutoGenerated: true, type: 'job', expression, handler, options }`
95
- - When loaded through `scheduler.load()`, auto-generated ids are replaced with the file route path, so `daily-report.schedule.ts` registers as `/daily-report`.
96
-
97
- #### `scheduler.add(id, expression | { delay }, handler, options?)`
98
-
99
- - `id: string | number` — 任务唯一标识,重复添加抛异常
100
- - `options.distributed.redis` — Redis 客户端
101
- - `options.distributed.ttl` — job 锁租约,单位毫秒
102
- - `options.distributed.namespace` — 可选,默认锁 key 的命名空间
103
- - `options.distributed.lockKey` — 可选,自定义锁 key,默认 `schedule:{namespace || 'default'}:{id}`
104
- - `options.onSkip(info)` — 没拿到锁时回调
105
- - `options.onError(error, info)` — handler 或锁流程异常时回调
106
-
107
- #### `scheduler.remove(id)`
108
-
109
- #### `scheduler.stop()`
110
-
111
- 取消所有任务
37
+ ## Boundaries
112
38
 
113
- #### `scheduler.getJobs(): JobInfo[]`
39
+ - Do not use default TypeORM or Redis services for multiple connections.
40
+ - Do not use `@hile/cache` without returning `new Cache(...)` from the loader function.
41
+ - Do not use `@hile/schedule` distributed mode without Redis lock TTLs sized for job duration.
114
42
 
115
- 返回已注册任务列表
43
+ - Returning raw values from `defineCache` handlers instead of `new Cache(value)`.
44
+ - Treating cache as source of truth.
45
+ - Forgetting to destroy manually created Redis or TypeORM clients.
46
+ - Scheduling jobs without idempotency when side effects can repeat.
116
47
 
117
- #### `scheduler.load(directory, options?)`
48
+ ## Verify
118
49
 
119
- 自动发现并注册任务文件,返回注销函数
50
+ - Manual Redis clients call `disconnect()` during cleanup.
51
+ - Manual TypeORM data sources call `destroy()` during cleanup.
52
+ - Cache keys include app/tenant prefixes when shared Redis is used.
53
+ - Scheduled jobs have clear duplicate-run policy.
120
54
 
121
- ## License
55
+ ## More Context
122
56
 
123
- MIT
57
+ - `AI.md` in this package: full package-local AI guide.
58
+ - Root `llms-full.txt`: full monorepo AI context.
59
+ - Root `references/`: source files copied from `docs/ai`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hile/schedule",
3
- "version": "3.0.1",
3
+ "version": "3.0.2",
4
4
  "description": "Declarative job scheduler based on node-schedule",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -11,7 +11,8 @@
11
11
  },
12
12
  "files": [
13
13
  "dist",
14
- "README.md"
14
+ "README.md",
15
+ "AI.md"
15
16
  ],
16
17
  "license": "MIT",
17
18
  "publishConfig": {
@@ -23,9 +24,9 @@
23
24
  "vitest": "^4.0.18"
24
25
  },
25
26
  "dependencies": {
26
- "@hile/loader": "^2.1.1",
27
- "@hile/redis-lock": "^3.0.1",
27
+ "@hile/loader": "^3.0.0",
28
+ "@hile/redis-lock": "^3.0.2",
28
29
  "node-schedule": "^2.1.1"
29
30
  },
30
- "gitHead": "88f52fb95743f86761778776aff23631fcf9d821"
31
+ "gitHead": "0985b6f8abc1f4de0a36324063585fdc3ac1375b"
31
32
  }