@chidchanun/bcp 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,357 @@
1
+ # Job Scheduling Platform
2
+
3
+ BCP Framework `0.2.9` extends the Background Jobs Platform with recurring job schedules, dependency-free UTC cron expressions, interval schedules and a schedule-store lease contract for multi-instance deployments.
4
+
5
+ Import scheduling APIs from the server-only `bcp/jobs` entrypoint:
6
+
7
+ ```ts
8
+ import {
9
+ createJobQueue,
10
+ createJobScheduler,
11
+ } from "bcp/jobs";
12
+ ```
13
+
14
+ ## Create a scheduler
15
+
16
+ A scheduler enqueues work into an existing BCP background job queue:
17
+
18
+ ```ts
19
+ import {
20
+ createJobQueue,
21
+ createJobScheduler,
22
+ } from "bcp/jobs";
23
+
24
+ export const jobs =
25
+ createJobQueue();
26
+
27
+ export const scheduler =
28
+ createJobScheduler({
29
+ queue: jobs,
30
+ });
31
+ ```
32
+
33
+ The scheduler does not execute application work itself. It creates normal queue jobs when a schedule becomes due; queue workers execute the registered job handlers.
34
+
35
+ ## Interval schedules
36
+
37
+ Schedule a recurring job every five minutes:
38
+
39
+ ```ts
40
+ await scheduler.schedule(
41
+ "cache.cleanup",
42
+ {
43
+ scope: "expired",
44
+ },
45
+ {
46
+ id: "cache-cleanup",
47
+ everyMs: 5 * 60 * 1000,
48
+ }
49
+ );
50
+ ```
51
+
52
+ `everyMs` must be a positive integer.
53
+
54
+ By default the first run occurs one interval after the schedule is created.
55
+
56
+ Use `startAt` to select the first due time explicitly:
57
+
58
+ ```ts
59
+ await scheduler.schedule(
60
+ "report.daily",
61
+ {},
62
+ {
63
+ everyMs:
64
+ 24 * 60 * 60 * 1000,
65
+ startAt:
66
+ Date.now() + 10_000,
67
+ }
68
+ );
69
+ ```
70
+
71
+ `startAt` accepts either a millisecond timestamp or a `Date`.
72
+
73
+ ## Cron schedules
74
+
75
+ BCP `0.2.9` supports dependency-free five-field cron expressions:
76
+
77
+ ```text
78
+ minute hour day-of-month month day-of-week
79
+ ```
80
+
81
+ Example — every weekday at 09:30 UTC:
82
+
83
+ ```ts
84
+ await scheduler.schedule(
85
+ "report.weekday",
86
+ {},
87
+ {
88
+ cron: "30 9 * * 1-5",
89
+ }
90
+ );
91
+ ```
92
+
93
+ Example — every 15 minutes:
94
+
95
+ ```ts
96
+ await scheduler.schedule(
97
+ "sync.incremental",
98
+ {},
99
+ {
100
+ cron: "*/15 * * * *",
101
+ }
102
+ );
103
+ ```
104
+
105
+ Supported field syntax:
106
+
107
+ ```text
108
+ * every allowed value
109
+ */n step
110
+ 1,5,10 list
111
+ 1-5 range
112
+ 1-10/2 range with step
113
+ ```
114
+
115
+ Ranges:
116
+
117
+ ```text
118
+ minute 0-59
119
+ hour 0-23
120
+ day-of-month 1-31
121
+ month 1-12
122
+ day-of-week 0-7 (0 and 7 are Sunday)
123
+ ```
124
+
125
+ Cron evaluation is **UTC** in `0.2.9`. This avoids machine-local timezone differences between containers and servers.
126
+
127
+ When both day-of-month and day-of-week are restricted, BCP follows conventional cron OR semantics: either field may match.
128
+
129
+ Use `nextCronTime()` when application tooling needs to preview the next UTC occurrence:
130
+
131
+ ```ts
132
+ import {
133
+ nextCronTime,
134
+ } from "bcp/jobs";
135
+
136
+ const next =
137
+ nextCronTime(
138
+ "0 2 * * *",
139
+ Date.now()
140
+ );
141
+ ```
142
+
143
+ ## Run due schedules manually
144
+
145
+ `runDue()` is useful for deterministic tests or externally driven schedulers:
146
+
147
+ ```ts
148
+ const count =
149
+ await scheduler.runDue();
150
+ ```
151
+
152
+ The return value is the number of newly enqueued jobs.
153
+
154
+ Options:
155
+
156
+ ```ts
157
+ await scheduler.runDue({
158
+ limit: 100,
159
+ leaseMs: 30_000,
160
+ });
161
+ ```
162
+
163
+ Each call claims at most one occurrence per due schedule. If an application was offline for many intervals, later calls may progressively catch up.
164
+
165
+ ## Start a scheduler runner
166
+
167
+ For a long-running Node.js process:
168
+
169
+ ```ts
170
+ const runner =
171
+ scheduler.start({
172
+ pollIntervalMs: 1_000,
173
+ leaseMs: 30_000,
174
+ limit: 100,
175
+ onError(error) {
176
+ console.error(
177
+ "scheduler failure",
178
+ error
179
+ );
180
+ },
181
+ });
182
+ ```
183
+
184
+ Graceful shutdown:
185
+
186
+ ```ts
187
+ await runner.stop();
188
+ await scheduler.close();
189
+ await jobs.close();
190
+ ```
191
+
192
+ Stopping a scheduler does not close the queue automatically because applications may run queue workers independently.
193
+
194
+ ## Schedule records
195
+
196
+ `JobScheduleRecord` includes:
197
+
198
+ ```text
199
+ id
200
+ jobName
201
+ payload
202
+ schedule
203
+ createdAt
204
+ updatedAt
205
+ nextRunAt
206
+ lastRunAt
207
+ maxAttempts
208
+ leaseOwner
209
+ leaseUntil
210
+ ```
211
+
212
+ Inspect schedules:
213
+
214
+ ```ts
215
+ const schedules =
216
+ await scheduler.list();
217
+
218
+ const schedule =
219
+ await scheduler.get(
220
+ "cache-cleanup"
221
+ );
222
+ ```
223
+
224
+ Remove one:
225
+
226
+ ```ts
227
+ await scheduler.remove(
228
+ "cache-cleanup"
229
+ );
230
+ ```
231
+
232
+ Calling `schedule()` with an existing schedule id is an upsert. Use stable ids for application-owned recurring schedules.
233
+
234
+ ## Retry policy
235
+
236
+ A schedule may forward a queue retry limit:
237
+
238
+ ```ts
239
+ await scheduler.schedule(
240
+ "billing.reconcile",
241
+ {},
242
+ {
243
+ cron: "0 * * * *",
244
+ maxAttempts: 5,
245
+ }
246
+ );
247
+ ```
248
+
249
+ Retry timing is still owned by `createJobQueue()`:
250
+
251
+ ```ts
252
+ const jobs =
253
+ createJobQueue({
254
+ retryDelayMs: attempt =>
255
+ attempt * 5_000,
256
+ });
257
+ ```
258
+
259
+ The scheduler determines **when a recurring occurrence is created**. The queue determines **how that occurrence is retried after handler failure**.
260
+
261
+ ## Deterministic scheduled run IDs
262
+
263
+ Scheduled occurrences receive deterministic queue ids:
264
+
265
+ ```text
266
+ schedule:<schedule-id>:<scheduled-for-timestamp>
267
+ ```
268
+
269
+ This provides an additional duplicate-enqueue defense if a scheduler retries the same occurrence.
270
+
271
+ Applications should avoid manually creating queue job ids with the `schedule:` prefix.
272
+
273
+ ## Schedule store contract
274
+
275
+ The built-in schedule store is process-local:
276
+
277
+ ```ts
278
+ import {
279
+ createMemoryJobScheduleStore,
280
+ } from "bcp/jobs";
281
+ ```
282
+
283
+ It is useful for development, tests and single-process applications.
284
+
285
+ Production deployments that run multiple scheduler processes or must preserve schedules across restarts should implement `JobScheduleStore` using shared durable infrastructure.
286
+
287
+ Required operations:
288
+
289
+ ```text
290
+ upsert
291
+ get
292
+ list
293
+ remove
294
+ acquireDue
295
+ complete
296
+ release
297
+ close (optional)
298
+ ```
299
+
300
+ `acquireDue()` is the critical concurrency boundary. A durable implementation should atomically claim due schedules and assign a lease owner/expiry so two scheduler instances do not both own the same occurrence.
301
+
302
+ Conceptually:
303
+
304
+ ```text
305
+ scheduler A ─┐
306
+ ├─ shared JobScheduleStore ── atomic acquireDue()
307
+ scheduler B ─┘
308
+
309
+
310
+ shared/durable queue
311
+ ```
312
+
313
+ Examples of possible application adapters include PostgreSQL, Redis, DynamoDB or another transactional/atomic data store. `0.2.9` intentionally does not force one provider.
314
+
315
+ ## Lease behavior
316
+
317
+ A scheduler runner identifies itself with `ownerId`:
318
+
319
+ ```ts
320
+ const scheduler =
321
+ createJobScheduler({
322
+ queue: jobs,
323
+ store,
324
+ ownerId:
325
+ process.env.INSTANCE_ID,
326
+ });
327
+ ```
328
+
329
+ If `ownerId` is omitted, BCP generates a unique process-local scheduler id.
330
+
331
+ A due schedule is leased for `leaseMs`. The store releases the lease after the occurrence is committed. If a scheduler dies while holding a lease, another instance may reclaim it after lease expiry.
332
+
333
+ For durable adapters, choose a lease duration longer than the expected enqueue/store round trip but short enough for acceptable recovery.
334
+
335
+ ## Queue durability remains separate
336
+
337
+ A durable schedule store does not make the queue durable by itself.
338
+
339
+ For multi-instance production you will normally need both:
340
+
341
+ ```text
342
+ shared JobScheduleStore
343
+ +
344
+ shared JobQueueAdapter
345
+ ```
346
+
347
+ The built-in memory schedule store and memory queue are intentionally process-local.
348
+
349
+ ## Server-only boundary
350
+
351
+ `bcp/jobs` is server-only. Do not import it from page components, client islands or other modules reachable from browser bundles.
352
+
353
+ Use job/scheduler calls from server routes, loaders/actions, server startup modules or dedicated worker processes.
354
+
355
+ ## Compatibility
356
+
357
+ `0.2.9` does not intentionally break `0.2.8` Background Jobs APIs. Existing queue/worker code remains valid; scheduling is additive.
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "framework": "bcp",
4
- "version": "0.2.7",
4
+ "version": "0.2.9",
5
5
  "releaseState": "unreleased",
6
- "baseline": "observability-platform-v2",
6
+ "baseline": "job-scheduling-platform",
7
7
  "runtime": {
8
8
  "node": ">=24.11.0",
9
9
  "react": "19",
@@ -19,6 +19,7 @@
19
19
  "bcp/error",
20
20
  "bcp/database",
21
21
  "bcp/auth",
22
+ "bcp/jobs",
22
23
  "bcp/observability",
23
24
  "bcp/server",
24
25
  "bcp/server-only",
@@ -65,6 +66,20 @@
65
66
  "prometheusMetrics": true,
66
67
  "requestMetricsMiddleware": true,
67
68
  "healthChecks": true,
69
+ "backgroundJobsPlatform": true,
70
+ "jobQueueAdapterContract": true,
71
+ "inMemoryJobQueue": true,
72
+ "delayedJobs": true,
73
+ "jobRetries": true,
74
+ "jobWorkerConcurrency": true,
75
+ "jobCancellation": true,
76
+ "jobSchedulingPlatform": true,
77
+ "recurringJobs": true,
78
+ "cronScheduling": true,
79
+ "intervalScheduling": true,
80
+ "jobScheduleStoreContract": true,
81
+ "schedulerLeases": true,
82
+ "scheduledRunDeduplication": true,
68
83
  "databaseMigrations": true,
69
84
  "databaseAdapterContract": true,
70
85
  "databasePostgresql": true,
@@ -104,7 +119,7 @@
104
119
  "s3-compatible"
105
120
  ],
106
121
  "compatibility": {
107
- "previousBaseline": "0.2.6",
122
+ "previousBaseline": "0.2.8",
108
123
  "intentionalBreakingChangesFromPreviousBaseline": false,
109
124
  "migrationGuide": "migration-0.2.md"
110
125
  },
@@ -121,7 +136,9 @@
121
136
  "authSessionStore": "auth-session-store.md",
122
137
  "authorizationSecurity": "authorization-security.md",
123
138
  "observability": "observability.md",
139
+ "backgroundJobs": "background-jobs.md",
140
+ "jobScheduling": "job-scheduling.md",
124
141
  "migrationGuide": "migration-0.2.md",
125
- "releaseNotes": "releases/0.2.7.md"
142
+ "releaseNotes": "releases/0.2.9.md"
126
143
  }
127
144
  }
@@ -0,0 +1,150 @@
1
+ # BCP Framework 0.2.8
2
+
3
+ **Milestone:** Background Jobs Platform
4
+
5
+ `0.2.8` adds a framework-native server-side background-job queue contract with a process-local reference adapter, delayed execution, retries/backoff, worker concurrency and cancellation.
6
+
7
+ > Release state: unreleased until the final RC sequence, tag and npm publication complete.
8
+
9
+ ## New public entrypoint
10
+
11
+ ```ts
12
+ import {
13
+ createJobQueue,
14
+ createMemoryJobQueueAdapter,
15
+ } from "bcp/jobs";
16
+ ```
17
+
18
+ `bcp/jobs` is server-only and is protected by both package browser exports and the BCP client-boundary validator.
19
+
20
+ ## Queue adapter contract
21
+
22
+ Applications and ecosystem packages can implement `JobQueueAdapter` for durable/shared infrastructure.
23
+
24
+ The contract covers:
25
+
26
+ ```text
27
+ enqueue
28
+ reserve
29
+ complete
30
+ fail
31
+ cancel
32
+ get
33
+ list
34
+ close (optional)
35
+ ```
36
+
37
+ `reserve()` is the adapter's atomic claim boundary for multi-worker processing.
38
+
39
+ ## In-memory reference adapter
40
+
41
+ `createMemoryJobQueueAdapter()` provides a dependency-free process-local adapter for development, tests and prototypes.
42
+
43
+ The memory adapter is intentionally not durable and cannot coordinate multiple Node.js processes or containers.
44
+
45
+ ## Delayed jobs
46
+
47
+ ```ts
48
+ await jobs.enqueue(
49
+ "report.generate",
50
+ payload,
51
+ {
52
+ delayMs: 60_000,
53
+ }
54
+ );
55
+ ```
56
+
57
+ Jobs are eligible for reservation only after `availableAt`.
58
+
59
+ ## Retry and backoff
60
+
61
+ Queues default to three maximum attempts and capped exponential retry delay.
62
+
63
+ Applications can configure a fixed delay or callback:
64
+
65
+ ```ts
66
+ createJobQueue({
67
+ defaultMaxAttempts: 5,
68
+ retryDelayMs: attempt =>
69
+ attempt * 5_000,
70
+ });
71
+ ```
72
+
73
+ Per-job `maxAttempts` overrides the queue default.
74
+
75
+ ## Workers
76
+
77
+ ```ts
78
+ const worker =
79
+ jobs.startWorker({
80
+ concurrency: 4,
81
+ pollIntervalMs: 250,
82
+ });
83
+ ```
84
+
85
+ Workers run concurrent processing loops and can be stopped gracefully.
86
+
87
+ `jobs.close()` stops all workers started by that queue and closes the adapter when supported.
88
+
89
+ ## Cancellation
90
+
91
+ ```ts
92
+ await jobs.cancel(jobId);
93
+ ```
94
+
95
+ Cancelled records remain terminal; later worker completion/failure writes do not overwrite cancellation in the memory adapter.
96
+
97
+ ## Manual processing
98
+
99
+ `processNext()` remains available for deterministic tests and custom supervisors.
100
+
101
+ ```ts
102
+ const processed =
103
+ await jobs.processNext();
104
+ ```
105
+
106
+ ## Package and platform contracts
107
+
108
+ `0.2.8` adds:
109
+
110
+ - `bcp/jobs` to the framework package export map,
111
+ - TypeScript path mapping for repository development,
112
+ - server-only client-boundary enforcement,
113
+ - Background Jobs capability metadata in `platform-manifest.json`,
114
+ - API-manifest ownership and docs-web navigation,
115
+ - prepared-package smoke coverage.
116
+
117
+ ## Compatibility
118
+
119
+ `0.2.8` does not intentionally remove or rename existing `0.2.7` public APIs.
120
+
121
+ Existing applications do not need to adopt background jobs.
122
+
123
+ The built-in queue is opt-in and no additional runtime dependency is installed.
124
+
125
+ ## Delivery semantics
126
+
127
+ Durable adapters should be designed for at-least-once processing. Job handlers should be idempotent when duplicate side effects are unsafe.
128
+
129
+ `0.2.8` does not promise exactly-once execution.
130
+
131
+ ## Not included
132
+
133
+ This milestone does not ship built-in adapters for Redis, PostgreSQL, SQS, RabbitMQ, Kafka, cron scheduling or workflow orchestration.
134
+
135
+ Those providers/features can be layered behind the new queue contract in later versions.
136
+
137
+ ## Validation
138
+
139
+ Before tagging/publishing `0.2.8` run:
140
+
141
+ ```bash
142
+ npm run typecheck
143
+ npm run test:unit
144
+ npm run test:integration
145
+ npm run test:e2e
146
+ npm run test:package
147
+ npm run rc:check
148
+ ```
149
+
150
+ The final release tag must point to the exact commit that passed the complete RC sequence.
@@ -0,0 +1,162 @@
1
+ # BCP Framework 0.2.9 — Job Scheduling Platform
2
+
3
+ > Release state: unreleased until the complete RC workflow passes, the release commit is tagged `v0.2.9`, and npm publication completes.
4
+
5
+ BCP Framework `0.2.9` extends the `0.2.8` Background Jobs Platform with recurring schedules, dependency-free UTC cron expressions and a lease-aware schedule-store contract.
6
+
7
+ ## Highlights
8
+
9
+ - Added `createJobScheduler()` through `bcp/jobs`.
10
+ - Added interval-based recurring jobs with `everyMs`.
11
+ - Added five-field UTC cron expressions.
12
+ - Added `nextCronTime()` and `nextScheduleTime()` helpers.
13
+ - Added `JobScheduleStore` for durable/shared scheduler state.
14
+ - Added `createMemoryJobScheduleStore()` for local development and tests.
15
+ - Added atomic-style `acquireDue()` lease semantics to the schedule-store contract.
16
+ - Added deterministic scheduled queue ids to reduce duplicate enqueue risk.
17
+ - Added scheduler polling lifecycle with `start()`, `stop()` and `close()`.
18
+ - Added schedule inspection, removal and stable-id upsert behavior.
19
+ - Added unit, package-smoke and platform-contract coverage.
20
+
21
+ ## Scheduling API
22
+
23
+ ```ts
24
+ import {
25
+ createJobQueue,
26
+ createJobScheduler,
27
+ } from "bcp/jobs";
28
+
29
+ const jobs =
30
+ createJobQueue();
31
+
32
+ const scheduler =
33
+ createJobScheduler({
34
+ queue: jobs,
35
+ });
36
+ ```
37
+
38
+ Interval:
39
+
40
+ ```ts
41
+ await scheduler.schedule(
42
+ "cache.cleanup",
43
+ {},
44
+ {
45
+ id: "cache-cleanup",
46
+ everyMs: 300_000,
47
+ }
48
+ );
49
+ ```
50
+
51
+ Cron:
52
+
53
+ ```ts
54
+ await scheduler.schedule(
55
+ "report.weekday",
56
+ {},
57
+ {
58
+ cron: "30 9 * * 1-5",
59
+ }
60
+ );
61
+ ```
62
+
63
+ Cron evaluation is UTC in `0.2.9`.
64
+
65
+ ## Cron syntax
66
+
67
+ Five fields are supported:
68
+
69
+ ```text
70
+ minute hour day-of-month month day-of-week
71
+ ```
72
+
73
+ Supported tokens:
74
+
75
+ ```text
76
+ *
77
+ */n
78
+ comma-separated values
79
+ ranges
80
+ range steps
81
+ ```
82
+
83
+ Day of week accepts `0` and `7` for Sunday. When both day-of-month and day-of-week are restricted, standard cron OR semantics are used.
84
+
85
+ ## Scheduler lifecycle
86
+
87
+ Long-running scheduler:
88
+
89
+ ```ts
90
+ const runner =
91
+ scheduler.start({
92
+ pollIntervalMs: 1_000,
93
+ leaseMs: 30_000,
94
+ });
95
+
96
+ // shutdown
97
+ await runner.stop();
98
+ await scheduler.close();
99
+ ```
100
+
101
+ `runDue()` is also available for tests, external control loops and deterministic execution.
102
+
103
+ ## Multi-instance contract
104
+
105
+ `JobScheduleStore.acquireDue()` is the scheduler concurrency boundary. Durable implementations should atomically claim due schedules with a lease owner and expiry.
106
+
107
+ The default memory store is process-local and is not suitable for schedules that must survive restarts or coordinate across multiple containers.
108
+
109
+ Production multi-instance systems will normally pair:
110
+
111
+ ```text
112
+ shared JobScheduleStore
113
+ +
114
+ shared JobQueueAdapter
115
+ ```
116
+
117
+ `0.2.9` intentionally leaves provider choice to the application instead of forcing Redis, PostgreSQL or another queue/scheduler backend.
118
+
119
+ ## Duplicate protection
120
+
121
+ Each scheduled occurrence uses a deterministic queue id:
122
+
123
+ ```text
124
+ schedule:<schedule-id>:<scheduled-for-timestamp>
125
+ ```
126
+
127
+ This complements store leasing and protects the built-in queue from enqueuing the same occurrence twice if the same schedule occurrence is retried.
128
+
129
+ ## Compatibility
130
+
131
+ This milestone is additive. There are no intentional breaking changes from `0.2.8`.
132
+
133
+ Existing `createJobQueue()`, handlers, delayed jobs, retries, workers and adapters continue to work without scheduling enabled.
134
+
135
+ ## Documentation
136
+
137
+ New guide:
138
+
139
+ ```text
140
+ docs/job-scheduling.md
141
+ ```
142
+
143
+ Background jobs remain documented in:
144
+
145
+ ```text
146
+ docs/background-jobs.md
147
+ ```
148
+
149
+ ## Validation
150
+
151
+ Before tagging/publishing `0.2.9`, run:
152
+
153
+ ```bash
154
+ npm run typecheck
155
+ npm run test:unit
156
+ npm run test:integration
157
+ npm run test:e2e
158
+ npm run test:package
159
+ npm run rc:check
160
+ ```
161
+
162
+ The release tag must point to the exact commit that passed the complete RC sequence.