@valentinkolb/sync 2.1.1 → 2.1.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.
- package/README.md +11 -0
- package/index.d.ts +1 -1
- package/index.js +94 -16
- package/package.json +1 -1
- package/src/scheduler.d.ts +23 -0
package/README.md
CHANGED
|
@@ -351,6 +351,11 @@ await sched.register({
|
|
|
351
351
|
misfire: "skip", // default: do not replay backlog
|
|
352
352
|
meta: { owner: "ops" },
|
|
353
353
|
});
|
|
354
|
+
|
|
355
|
+
await sched.triggerNow({
|
|
356
|
+
id: "cleanup-hourly",
|
|
357
|
+
key: "ops-manual-run-1", // optional but recommended for retry-safe manual triggers
|
|
358
|
+
});
|
|
354
359
|
```
|
|
355
360
|
|
|
356
361
|
### Scheduler features
|
|
@@ -358,10 +363,16 @@ await sched.register({
|
|
|
358
363
|
- **Idempotent upsert registration**: repeated `register({ id, ... })` creates once, then updates in place.
|
|
359
364
|
- **No fixed leader pod**: leadership uses a renewable Redis lease (`mutex`) with epoch fencing.
|
|
360
365
|
- **Durable dispatch**: each cron slot maps to deterministic job key (`scheduleId:slotTs`) to prevent duplicates.
|
|
366
|
+
- **Durable manual trigger**: `triggerNow({ id, key? })` submits immediately through the same durable job path. Durability begins once `triggerNow()` returns a `jobId`.
|
|
361
367
|
- **Misfire policies**: `skip` (default), `catch_up_one`, `catch_up_all` with cap (`maxCatchUpRuns`).
|
|
362
368
|
- **Failure isolation**: submit retry + backoff, dispatch DLQ, configurable threshold for auto-advance after repeated failures.
|
|
363
369
|
- **Handler safety**: optional `strictHandlers` mode (default `true`) relinquishes leadership when required handlers are missing.
|
|
364
370
|
|
|
371
|
+
Manual trigger notes:
|
|
372
|
+
- `triggerNow()` does not require `start()` and does not alter cron state (`nextRunAt`, misfire handling, due slots).
|
|
373
|
+
- `triggerNow()` reuses the registered schedule input. If you need custom input per run, call the underlying `job.submit(...)` directly.
|
|
374
|
+
- Pass `key` for retry-safe idempotent manual triggering. Without `key`, repeated calls create additional runs.
|
|
375
|
+
|
|
365
376
|
## Ephemeral
|
|
366
377
|
|
|
367
378
|
Typed ephemeral key/value store with TTL semantics and stream events. Useful for short-lived state like presence, worker heartbeats, or temporary coordination hints.
|
package/index.d.ts
CHANGED
|
@@ -3,6 +3,6 @@ export { mutex, LockError, type Mutex, type Lock, type MutexConfig } from "./src
|
|
|
3
3
|
export { queue, type Queue, type QueueConfig, type QueueReader, type QueueRecvConfig, type QueueSendConfig, type QueueReceived, } from "./src/queue";
|
|
4
4
|
export { topic, type Topic, type TopicConfig, type TopicReader, type TopicRecvConfig, type TopicPubConfig, type TopicDelivery, type TopicLiveConfig, type TopicLiveEvent, } from "./src/topic";
|
|
5
5
|
export { job, type JobId, type JobStatus, type JobTerminal, type SubmitOptions, type JoinOptions, type CancelOptions, type JobEvent, type JobEvents, type JobContext, type JobHandle, type JobDefinition, } from "./src/job";
|
|
6
|
-
export { scheduler, type Scheduler, type SchedulerConfig, type SchedulerRegisterConfig, type SchedulerUnregisterConfig, type SchedulerGetConfig, type SchedulerInfo, type SchedulerMetric, type SchedulerMetricsSnapshot, } from "./src/scheduler";
|
|
6
|
+
export { scheduler, type Scheduler, type SchedulerConfig, type SchedulerRegisterConfig, type SchedulerUnregisterConfig, type SchedulerTriggerNowConfig, type SchedulerGetConfig, type SchedulerInfo, type SchedulerMetric, type SchedulerMetricsSnapshot, } from "./src/scheduler";
|
|
7
7
|
export { ephemeral, EphemeralCapacityError, EphemeralPayloadTooLargeError, type EphemeralConfig, type EphemeralUpsertConfig, type EphemeralTouchConfig, type EphemeralRemoveConfig, type EphemeralEntry, type EphemeralSnapshot, type EphemeralRecvConfig, type EphemeralEvent, type EphemeralReader, type EphemeralStore, } from "./src/ephemeral";
|
|
8
8
|
export { retry, isRetryableTransportError, DEFAULT_RETRY_OPTIONS, type RetryOptions, } from "./src/retry";
|
package/index.js
CHANGED
|
@@ -15745,6 +15745,9 @@ var scheduler = (config2) => {
|
|
|
15745
15745
|
dispatchRetried: 0,
|
|
15746
15746
|
dispatchSkipped: 0,
|
|
15747
15747
|
dispatchDlq: 0,
|
|
15748
|
+
triggerSubmitted: 0,
|
|
15749
|
+
triggerFailed: 0,
|
|
15750
|
+
triggerRejected: 0,
|
|
15748
15751
|
tickErrors: 0,
|
|
15749
15752
|
lastTickAt: null
|
|
15750
15753
|
};
|
|
@@ -15884,22 +15887,17 @@ var scheduler = (config2) => {
|
|
|
15884
15887
|
message: cfg.message
|
|
15885
15888
|
});
|
|
15886
15889
|
};
|
|
15887
|
-
const
|
|
15888
|
-
await retry(async () => {
|
|
15889
|
-
if (!await ensureLeadership({ forceRefresh: true })) {
|
|
15890
|
+
const submitScheduledJob = async (cfg) => {
|
|
15891
|
+
return await retry(async () => {
|
|
15892
|
+
if (cfg.requireLeadership && !await ensureLeadership({ forceRefresh: true })) {
|
|
15890
15893
|
throw new Error("leadership lost during dispatch");
|
|
15891
15894
|
}
|
|
15892
15895
|
return await cfg.jobHandle.submit({
|
|
15893
15896
|
input: cfg.schedule.input,
|
|
15894
|
-
key:
|
|
15897
|
+
key: cfg.key,
|
|
15895
15898
|
keyTtlMs: scheduledJobKeyTtlMs,
|
|
15896
|
-
at: cfg.
|
|
15897
|
-
meta:
|
|
15898
|
-
...cfg.schedule.meta ?? {},
|
|
15899
|
-
scheduleId: cfg.schedule.id,
|
|
15900
|
-
scheduleSlotTs: cfg.slotTs,
|
|
15901
|
-
schedulerId: config2.id
|
|
15902
|
-
}
|
|
15899
|
+
...cfg.at !== undefined ? { at: cfg.at } : {},
|
|
15900
|
+
meta: cfg.meta
|
|
15903
15901
|
});
|
|
15904
15902
|
}, {
|
|
15905
15903
|
attempts: submitRetries + 1,
|
|
@@ -15911,9 +15909,9 @@ var scheduler = (config2) => {
|
|
|
15911
15909
|
const err = asError4(error48);
|
|
15912
15910
|
if (err.name === "ZodError")
|
|
15913
15911
|
return false;
|
|
15914
|
-
if (err.message === "leadership lost during dispatch")
|
|
15912
|
+
if (cfg.requireLeadership && err.message === "leadership lost during dispatch")
|
|
15915
15913
|
return false;
|
|
15916
|
-
|
|
15914
|
+
cfg.onRetry?.();
|
|
15917
15915
|
return true;
|
|
15918
15916
|
}
|
|
15919
15917
|
});
|
|
@@ -16000,10 +15998,21 @@ var scheduler = (config2) => {
|
|
|
16000
15998
|
break;
|
|
16001
15999
|
}
|
|
16002
16000
|
try {
|
|
16003
|
-
await
|
|
16001
|
+
const jobId = await submitScheduledJob({
|
|
16004
16002
|
jobHandle,
|
|
16005
16003
|
schedule,
|
|
16006
|
-
slotTs
|
|
16004
|
+
key: `${schedule.id}:${slotTs}`,
|
|
16005
|
+
at: slotTs,
|
|
16006
|
+
meta: {
|
|
16007
|
+
...schedule.meta ?? {},
|
|
16008
|
+
scheduleId: schedule.id,
|
|
16009
|
+
scheduleSlotTs: slotTs,
|
|
16010
|
+
schedulerId: config2.id
|
|
16011
|
+
},
|
|
16012
|
+
requireLeadership: true,
|
|
16013
|
+
onRetry: () => {
|
|
16014
|
+
metricsState.dispatchRetried += 1;
|
|
16015
|
+
}
|
|
16007
16016
|
});
|
|
16008
16017
|
metricsState.dispatchSubmitted += 1;
|
|
16009
16018
|
submitsRemaining -= 1;
|
|
@@ -16014,7 +16023,7 @@ var scheduler = (config2) => {
|
|
|
16014
16023
|
ts: Date.now(),
|
|
16015
16024
|
scheduleId: schedule.id,
|
|
16016
16025
|
slotTs,
|
|
16017
|
-
jobId
|
|
16026
|
+
jobId
|
|
16018
16027
|
});
|
|
16019
16028
|
} catch (error48) {
|
|
16020
16029
|
submitFailed = true;
|
|
@@ -16188,6 +16197,74 @@ var scheduler = (config2) => {
|
|
|
16188
16197
|
jobsById.delete(jobId);
|
|
16189
16198
|
}
|
|
16190
16199
|
};
|
|
16200
|
+
const triggerNow = async (cfg) => {
|
|
16201
|
+
const raw = await redis6.get(scheduleKey(cfg.id));
|
|
16202
|
+
if (!raw) {
|
|
16203
|
+
metricsState.triggerRejected += 1;
|
|
16204
|
+
safeMetric(config2.onMetric, {
|
|
16205
|
+
type: "trigger_rejected",
|
|
16206
|
+
ts: Date.now(),
|
|
16207
|
+
scheduleId: cfg.id,
|
|
16208
|
+
reason: "missing_schedule"
|
|
16209
|
+
});
|
|
16210
|
+
throw new Error(`scheduler trigger rejected: missing schedule ${cfg.id}`);
|
|
16211
|
+
}
|
|
16212
|
+
const schedule = parseSchedule(raw);
|
|
16213
|
+
if (!schedule) {
|
|
16214
|
+
metricsState.triggerRejected += 1;
|
|
16215
|
+
safeMetric(config2.onMetric, {
|
|
16216
|
+
type: "trigger_rejected",
|
|
16217
|
+
ts: Date.now(),
|
|
16218
|
+
scheduleId: cfg.id,
|
|
16219
|
+
reason: "invalid_schedule"
|
|
16220
|
+
});
|
|
16221
|
+
throw new Error(`scheduler trigger rejected: invalid schedule ${cfg.id}`);
|
|
16222
|
+
}
|
|
16223
|
+
const jobHandle = jobsById.get(schedule.jobId);
|
|
16224
|
+
if (!jobHandle) {
|
|
16225
|
+
metricsState.triggerRejected += 1;
|
|
16226
|
+
safeMetric(config2.onMetric, {
|
|
16227
|
+
type: "trigger_rejected",
|
|
16228
|
+
ts: Date.now(),
|
|
16229
|
+
scheduleId: schedule.id,
|
|
16230
|
+
reason: "missing_handler"
|
|
16231
|
+
});
|
|
16232
|
+
throw new Error(`scheduler trigger rejected: missing local handler for schedule ${schedule.id}`);
|
|
16233
|
+
}
|
|
16234
|
+
try {
|
|
16235
|
+
const jobId = await submitScheduledJob({
|
|
16236
|
+
jobHandle,
|
|
16237
|
+
schedule,
|
|
16238
|
+
key: cfg.key ? `manual:${schedule.id}:${cfg.key}` : undefined,
|
|
16239
|
+
meta: {
|
|
16240
|
+
...schedule.meta ?? {},
|
|
16241
|
+
scheduleId: schedule.id,
|
|
16242
|
+
schedulerId: config2.id,
|
|
16243
|
+
scheduleTrigger: "manual",
|
|
16244
|
+
...cfg.key ? { scheduleManualKey: cfg.key } : {}
|
|
16245
|
+
},
|
|
16246
|
+
requireLeadership: false
|
|
16247
|
+
});
|
|
16248
|
+
metricsState.triggerSubmitted += 1;
|
|
16249
|
+
safeMetric(config2.onMetric, {
|
|
16250
|
+
type: "trigger_submitted",
|
|
16251
|
+
ts: Date.now(),
|
|
16252
|
+
scheduleId: schedule.id,
|
|
16253
|
+
jobId
|
|
16254
|
+
});
|
|
16255
|
+
return jobId;
|
|
16256
|
+
} catch (error48) {
|
|
16257
|
+
metricsState.triggerFailed += 1;
|
|
16258
|
+
const err = asError4(error48);
|
|
16259
|
+
safeMetric(config2.onMetric, {
|
|
16260
|
+
type: "trigger_failed",
|
|
16261
|
+
ts: Date.now(),
|
|
16262
|
+
scheduleId: schedule.id,
|
|
16263
|
+
message: err.message
|
|
16264
|
+
});
|
|
16265
|
+
throw err;
|
|
16266
|
+
}
|
|
16267
|
+
};
|
|
16191
16268
|
const get = async (cfg) => {
|
|
16192
16269
|
const raw = await redis6.get(scheduleKey(cfg.id));
|
|
16193
16270
|
const parsed = parseSchedule(raw);
|
|
@@ -16241,6 +16318,7 @@ var scheduler = (config2) => {
|
|
|
16241
16318
|
stop,
|
|
16242
16319
|
register,
|
|
16243
16320
|
unregister,
|
|
16321
|
+
triggerNow,
|
|
16244
16322
|
get,
|
|
16245
16323
|
list,
|
|
16246
16324
|
metrics
|
package/package.json
CHANGED
package/src/scheduler.d.ts
CHANGED
|
@@ -63,6 +63,21 @@ export type SchedulerMetric = {
|
|
|
63
63
|
scheduleId: string;
|
|
64
64
|
slotTs: number;
|
|
65
65
|
failures: number;
|
|
66
|
+
} | {
|
|
67
|
+
type: "trigger_submitted";
|
|
68
|
+
ts: number;
|
|
69
|
+
scheduleId: string;
|
|
70
|
+
jobId: string;
|
|
71
|
+
} | {
|
|
72
|
+
type: "trigger_rejected";
|
|
73
|
+
ts: number;
|
|
74
|
+
scheduleId: string;
|
|
75
|
+
reason: "missing_schedule" | "missing_handler" | "invalid_schedule";
|
|
76
|
+
} | {
|
|
77
|
+
type: "trigger_failed";
|
|
78
|
+
ts: number;
|
|
79
|
+
scheduleId: string;
|
|
80
|
+
message: string;
|
|
66
81
|
};
|
|
67
82
|
export type SchedulerConfig = {
|
|
68
83
|
id: string;
|
|
@@ -98,6 +113,10 @@ export type SchedulerRegisterConfig = {
|
|
|
98
113
|
export type SchedulerUnregisterConfig = {
|
|
99
114
|
id: string;
|
|
100
115
|
};
|
|
116
|
+
export type SchedulerTriggerNowConfig = {
|
|
117
|
+
id: string;
|
|
118
|
+
key?: string;
|
|
119
|
+
};
|
|
101
120
|
export type SchedulerGetConfig = {
|
|
102
121
|
id: string;
|
|
103
122
|
};
|
|
@@ -121,6 +140,9 @@ export type SchedulerMetricsSnapshot = {
|
|
|
121
140
|
dispatchRetried: number;
|
|
122
141
|
dispatchSkipped: number;
|
|
123
142
|
dispatchDlq: number;
|
|
143
|
+
triggerSubmitted: number;
|
|
144
|
+
triggerFailed: number;
|
|
145
|
+
triggerRejected: number;
|
|
124
146
|
tickErrors: number;
|
|
125
147
|
lastTickAt: number | null;
|
|
126
148
|
};
|
|
@@ -133,6 +155,7 @@ export type Scheduler = {
|
|
|
133
155
|
updated: boolean;
|
|
134
156
|
}>;
|
|
135
157
|
unregister(cfg: SchedulerUnregisterConfig): Promise<void>;
|
|
158
|
+
triggerNow(cfg: SchedulerTriggerNowConfig): Promise<string>;
|
|
136
159
|
get(cfg: SchedulerGetConfig): Promise<SchedulerInfo | null>;
|
|
137
160
|
list(): Promise<SchedulerInfo[]>;
|
|
138
161
|
metrics(): SchedulerMetricsSnapshot;
|