@polpo-ai/core 0.15.45 → 0.15.47
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 +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/mission-executor.d.ts +8 -2
- package/dist/mission-executor.d.ts.map +1 -1
- package/dist/mission-executor.js.map +1 -1
- package/dist/orchestrator-engine.d.ts +4 -0
- package/dist/orchestrator-engine.d.ts.map +1 -1
- package/dist/orchestrator-engine.js.map +1 -1
- package/dist/scheduling/dispatcher.d.ts +45 -0
- package/dist/scheduling/dispatcher.d.ts.map +1 -0
- package/dist/scheduling/dispatcher.js +201 -0
- package/dist/scheduling/dispatcher.js.map +1 -0
- package/dist/scheduling/dispatcher.test.d.ts +2 -0
- package/dist/scheduling/dispatcher.test.d.ts.map +1 -0
- package/dist/scheduling/dispatcher.test.js +378 -0
- package/dist/scheduling/dispatcher.test.js.map +1 -0
- package/dist/scheduling/driver.d.ts +20 -0
- package/dist/scheduling/driver.d.ts.map +1 -0
- package/dist/scheduling/driver.js +2 -0
- package/dist/scheduling/driver.js.map +1 -0
- package/dist/scheduling/index.d.ts +9 -0
- package/dist/scheduling/index.d.ts.map +1 -0
- package/dist/scheduling/index.js +8 -0
- package/dist/scheduling/index.js.map +1 -0
- package/dist/scheduling/occurrence.d.ts +17 -0
- package/dist/scheduling/occurrence.d.ts.map +1 -0
- package/dist/scheduling/occurrence.js +164 -0
- package/dist/scheduling/occurrence.js.map +1 -0
- package/dist/scheduling/occurrence.test.d.ts +2 -0
- package/dist/scheduling/occurrence.test.d.ts.map +1 -0
- package/dist/scheduling/occurrence.test.js +148 -0
- package/dist/scheduling/occurrence.test.js.map +1 -0
- package/dist/scheduling/state-machine.d.ts +10 -0
- package/dist/scheduling/state-machine.d.ts.map +1 -0
- package/dist/scheduling/state-machine.js +36 -0
- package/dist/scheduling/state-machine.js.map +1 -0
- package/dist/scheduling/state-machine.test.d.ts +2 -0
- package/dist/scheduling/state-machine.test.d.ts.map +1 -0
- package/dist/scheduling/state-machine.test.js +77 -0
- package/dist/scheduling/state-machine.test.js.map +1 -0
- package/dist/scheduling/store.d.ts +100 -0
- package/dist/scheduling/store.d.ts.map +1 -0
- package/dist/scheduling/store.js +635 -0
- package/dist/scheduling/store.js.map +1 -0
- package/dist/scheduling/store.test.d.ts +2 -0
- package/dist/scheduling/store.test.d.ts.map +1 -0
- package/dist/scheduling/store.test.js +462 -0
- package/dist/scheduling/store.test.js.map +1 -0
- package/dist/scheduling/types.d.ts +195 -0
- package/dist/scheduling/types.d.ts.map +1 -0
- package/dist/scheduling/types.js +2 -0
- package/dist/scheduling/types.js.map +1 -0
- package/dist/scheduling/validation.d.ts +28 -0
- package/dist/scheduling/validation.d.ts.map +1 -0
- package/dist/scheduling/validation.js +614 -0
- package/dist/scheduling/validation.js.map +1 -0
- package/dist/scheduling/validation.test.d.ts +2 -0
- package/dist/scheduling/validation.test.d.ts.map +1 -0
- package/dist/scheduling/validation.test.js +576 -0
- package/dist/scheduling/validation.test.js.map +1 -0
- package/dist/types/mission.d.ts +13 -3
- package/dist/types/mission.d.ts.map +1 -1
- package/package.json +6 -1
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
import { nanoid } from "nanoid";
|
|
2
|
+
import { assertScheduleRunStatusTransition, assertScheduleStatusTransition, isTerminalScheduleRunStatus, } from "./state-machine.js";
|
|
3
|
+
import { normalizeCreateScheduleInput, normalizeScheduleMetadata, normalizeUpdateScheduleInput, } from "./validation.js";
|
|
4
|
+
export class ScheduleStoreError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
constructor(message, code) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.name = new.target.name;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class ScheduleNotFoundError extends ScheduleStoreError {
|
|
13
|
+
constructor(entity, id) {
|
|
14
|
+
super(`${entity} "${id}" was not found`, "NOT_FOUND");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class ScheduleConflictError extends ScheduleStoreError {
|
|
18
|
+
constructor(message) {
|
|
19
|
+
super(message, "CONFLICT");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export class ScheduleInvalidStateError extends ScheduleStoreError {
|
|
23
|
+
constructor(message) {
|
|
24
|
+
super(message, "INVALID_STATE");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export class InMemoryScheduleStore {
|
|
28
|
+
schedules = new Map();
|
|
29
|
+
runs = new Map();
|
|
30
|
+
runIdsByIdempotencyKey = new Map();
|
|
31
|
+
options;
|
|
32
|
+
constructor(options = {}) {
|
|
33
|
+
this.options = {
|
|
34
|
+
now: options.now ?? (() => new Date()),
|
|
35
|
+
createId: options.createId ?? ((kind) => `${kind}-${nanoid()}`),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
async create(input) {
|
|
39
|
+
const now = this.now();
|
|
40
|
+
const normalized = normalizeCreateScheduleInput(input, { now });
|
|
41
|
+
const id = normalized.id ?? this.options.createId("schedule");
|
|
42
|
+
if (this.schedules.has(id)) {
|
|
43
|
+
throw new ScheduleConflictError(`Schedule "${id}" already exists`);
|
|
44
|
+
}
|
|
45
|
+
const schedule = {
|
|
46
|
+
id,
|
|
47
|
+
...(normalized.name === undefined ? {} : { name: normalized.name }),
|
|
48
|
+
...(normalized.description === undefined
|
|
49
|
+
? {}
|
|
50
|
+
: { description: normalized.description }),
|
|
51
|
+
timing: normalized.timing,
|
|
52
|
+
invocation: normalized.invocation,
|
|
53
|
+
status: normalized.status,
|
|
54
|
+
policy: normalized.policy,
|
|
55
|
+
metadata: normalized.metadata,
|
|
56
|
+
createdAt: now.toISOString(),
|
|
57
|
+
updatedAt: now.toISOString(),
|
|
58
|
+
revision: 1,
|
|
59
|
+
};
|
|
60
|
+
this.schedules.set(id, clone(schedule));
|
|
61
|
+
return clone(schedule);
|
|
62
|
+
}
|
|
63
|
+
async list(filter = {}) {
|
|
64
|
+
const statuses = normalizeStatusFilter(filter.status);
|
|
65
|
+
return [...this.schedules.values()]
|
|
66
|
+
.filter((schedule) => (filter.includeDeleted || schedule.status !== "deleted")
|
|
67
|
+
&& (!statuses || statuses.has(schedule.status))
|
|
68
|
+
&& (!filter.surface || schedule.invocation.surface === filter.surface))
|
|
69
|
+
.sort(compareCreated)
|
|
70
|
+
.map(clone);
|
|
71
|
+
}
|
|
72
|
+
async get(id) {
|
|
73
|
+
const schedule = this.schedules.get(id);
|
|
74
|
+
return schedule ? clone(schedule) : null;
|
|
75
|
+
}
|
|
76
|
+
async update(id, patch, options = {}) {
|
|
77
|
+
const existing = this.requireSchedule(id);
|
|
78
|
+
assertRevision(existing, options.expectedRevision);
|
|
79
|
+
if (existing.status === "deleted") {
|
|
80
|
+
throw new ScheduleInvalidStateError(`Schedule "${id}" is deleted`);
|
|
81
|
+
}
|
|
82
|
+
const now = this.now();
|
|
83
|
+
const normalized = normalizeUpdateScheduleInput(patch, { now });
|
|
84
|
+
const nextStatus = normalized.status ?? existing.status;
|
|
85
|
+
try {
|
|
86
|
+
assertScheduleStatusTransition(existing.status, nextStatus);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
throw new ScheduleInvalidStateError(errorMessage(error));
|
|
90
|
+
}
|
|
91
|
+
const updated = {
|
|
92
|
+
...existing,
|
|
93
|
+
...(normalized.timing === undefined ? {} : { timing: normalized.timing }),
|
|
94
|
+
...(normalized.invocation === undefined
|
|
95
|
+
? {}
|
|
96
|
+
: { invocation: normalized.invocation }),
|
|
97
|
+
...(normalized.status === undefined ? {} : { status: normalized.status }),
|
|
98
|
+
...(normalized.policy === undefined
|
|
99
|
+
? {}
|
|
100
|
+
: { policy: { ...existing.policy, ...normalized.policy } }),
|
|
101
|
+
...(normalized.metadata === undefined
|
|
102
|
+
? {}
|
|
103
|
+
: { metadata: normalized.metadata }),
|
|
104
|
+
updatedAt: now.toISOString(),
|
|
105
|
+
revision: existing.revision + 1,
|
|
106
|
+
};
|
|
107
|
+
applyNullableStringPatch(updated, "name", normalized.name);
|
|
108
|
+
applyNullableStringPatch(updated, "description", normalized.description);
|
|
109
|
+
this.schedules.set(id, clone(updated));
|
|
110
|
+
return clone(updated);
|
|
111
|
+
}
|
|
112
|
+
async markDeleted(id, options = {}) {
|
|
113
|
+
const existing = this.requireSchedule(id);
|
|
114
|
+
assertRevision(existing, options.expectedRevision);
|
|
115
|
+
if (existing.status === "deleted")
|
|
116
|
+
return;
|
|
117
|
+
try {
|
|
118
|
+
assertScheduleStatusTransition(existing.status, "deleted");
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
throw new ScheduleInvalidStateError(errorMessage(error));
|
|
122
|
+
}
|
|
123
|
+
const now = this.now().toISOString();
|
|
124
|
+
this.schedules.set(id, {
|
|
125
|
+
...existing,
|
|
126
|
+
status: "deleted",
|
|
127
|
+
updatedAt: now,
|
|
128
|
+
revision: existing.revision + 1,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
async updateOperationalState(id, patch, options = {}) {
|
|
132
|
+
const existing = this.requireSchedule(id);
|
|
133
|
+
assertRevision(existing, options.expectedRevision);
|
|
134
|
+
const normalized = normalizeScheduleOperationalStatePatch(patch);
|
|
135
|
+
const updated = {
|
|
136
|
+
...existing,
|
|
137
|
+
revision: existing.revision + 1,
|
|
138
|
+
updatedAt: this.now().toISOString(),
|
|
139
|
+
};
|
|
140
|
+
applyNullableField(updated, "nextOccurrenceAt", normalized.nextOccurrenceAt);
|
|
141
|
+
applyNullableField(updated, "lastOccurrenceAt", normalized.lastOccurrenceAt);
|
|
142
|
+
applyNullableField(updated, "driver", normalized.driver);
|
|
143
|
+
this.schedules.set(id, clone(updated));
|
|
144
|
+
return clone(updated);
|
|
145
|
+
}
|
|
146
|
+
async createRun(input) {
|
|
147
|
+
const normalized = normalizeCreateScheduleRunInput(input);
|
|
148
|
+
const duplicateId = this.runIdsByIdempotencyKey.get(normalized.idempotencyKey);
|
|
149
|
+
if (duplicateId) {
|
|
150
|
+
const duplicate = this.runs.get(duplicateId);
|
|
151
|
+
if (!duplicate) {
|
|
152
|
+
throw new ScheduleInvalidStateError(`Schedule run idempotency index is corrupted for "${normalized.idempotencyKey}"`);
|
|
153
|
+
}
|
|
154
|
+
assertSameScheduleOccurrence(duplicate, normalized);
|
|
155
|
+
return clone(duplicate);
|
|
156
|
+
}
|
|
157
|
+
const schedule = this.requireSchedule(normalized.scheduleId);
|
|
158
|
+
if (schedule.status !== "active") {
|
|
159
|
+
throw new ScheduleInvalidStateError(`Schedule "${schedule.id}" is not active`);
|
|
160
|
+
}
|
|
161
|
+
const id = normalized.id ?? this.options.createId("schedule-run");
|
|
162
|
+
if (this.runs.has(id)) {
|
|
163
|
+
throw new ScheduleConflictError(`Schedule run "${id}" already exists`);
|
|
164
|
+
}
|
|
165
|
+
const now = this.now().toISOString();
|
|
166
|
+
const run = {
|
|
167
|
+
id,
|
|
168
|
+
scheduleId: normalized.scheduleId,
|
|
169
|
+
occurrenceAt: normalized.occurrenceAt,
|
|
170
|
+
triggerId: normalized.triggerId,
|
|
171
|
+
idempotencyKey: normalized.idempotencyKey,
|
|
172
|
+
status: "pending",
|
|
173
|
+
attempts: 0,
|
|
174
|
+
references: {},
|
|
175
|
+
createdAt: now,
|
|
176
|
+
updatedAt: now,
|
|
177
|
+
};
|
|
178
|
+
this.runs.set(id, clone(run));
|
|
179
|
+
this.runIdsByIdempotencyKey.set(run.idempotencyKey, id);
|
|
180
|
+
return clone(run);
|
|
181
|
+
}
|
|
182
|
+
async getRun(id) {
|
|
183
|
+
const run = this.runs.get(id);
|
|
184
|
+
return run ? clone(run) : null;
|
|
185
|
+
}
|
|
186
|
+
async listRuns(filter = {}) {
|
|
187
|
+
const statuses = normalizeRunStatusFilter(filter.status);
|
|
188
|
+
const limit = normalizeLimit(filter.limit);
|
|
189
|
+
const direction = normalizeRunOrder(filter.order);
|
|
190
|
+
return [...this.runs.values()]
|
|
191
|
+
.filter((run) => (!filter.scheduleId || run.scheduleId === filter.scheduleId)
|
|
192
|
+
&& (!statuses || statuses.has(run.status)))
|
|
193
|
+
.sort((a, b) => direction * (a.occurrenceAt.localeCompare(b.occurrenceAt)
|
|
194
|
+
|| a.createdAt.localeCompare(b.createdAt)
|
|
195
|
+
|| a.id.localeCompare(b.id)))
|
|
196
|
+
.slice(0, limit)
|
|
197
|
+
.map(clone);
|
|
198
|
+
}
|
|
199
|
+
async claimRun(id, lease) {
|
|
200
|
+
const now = this.now();
|
|
201
|
+
const normalizedLease = normalizeScheduleLease(lease, now);
|
|
202
|
+
const run = this.requireRun(id);
|
|
203
|
+
if (isTerminalScheduleRunStatus(run.status))
|
|
204
|
+
return null;
|
|
205
|
+
const schedule = this.requireSchedule(run.scheduleId);
|
|
206
|
+
if (schedule.status !== "active")
|
|
207
|
+
return null;
|
|
208
|
+
if ((run.status === "claimed" || run.status === "running")
|
|
209
|
+
&& run.lease
|
|
210
|
+
&& Date.parse(run.lease.expiresAt) > now.getTime()) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
if (run.status !== "pending"
|
|
214
|
+
&& run.status !== "claimed"
|
|
215
|
+
&& run.status !== "running") {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
const activeRuns = [...this.runs.values()].filter((candidate) => candidate.id !== run.id
|
|
219
|
+
&& candidate.scheduleId === run.scheduleId
|
|
220
|
+
&& isActiveRun(candidate)
|
|
221
|
+
&& Boolean(candidate.lease)
|
|
222
|
+
&& Date.parse(candidate.lease.expiresAt) > now.getTime()).length;
|
|
223
|
+
if (activeRuns >= schedule.policy.maxConcurrency)
|
|
224
|
+
return null;
|
|
225
|
+
try {
|
|
226
|
+
assertScheduleRunStatusTransition(run.status, "claimed");
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
throw new ScheduleInvalidStateError(errorMessage(error));
|
|
230
|
+
}
|
|
231
|
+
const claimed = {
|
|
232
|
+
...run,
|
|
233
|
+
status: "claimed",
|
|
234
|
+
attempts: run.attempts + 1,
|
|
235
|
+
lease: normalizedLease,
|
|
236
|
+
updatedAt: now.toISOString(),
|
|
237
|
+
};
|
|
238
|
+
this.runs.set(id, clone(claimed));
|
|
239
|
+
return clone(claimed);
|
|
240
|
+
}
|
|
241
|
+
async renewLease(id, lease) {
|
|
242
|
+
const now = this.now();
|
|
243
|
+
const normalizedLease = normalizeScheduleLease(lease, now);
|
|
244
|
+
const run = this.requireRun(id);
|
|
245
|
+
if (!isActiveRun(run) || !leaseMatches(run.lease, normalizedLease))
|
|
246
|
+
return false;
|
|
247
|
+
if (!run.lease || Date.parse(run.lease.expiresAt) <= now.getTime())
|
|
248
|
+
return false;
|
|
249
|
+
if (Date.parse(normalizedLease.expiresAt) < Date.parse(run.lease.expiresAt)) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
this.runs.set(id, {
|
|
253
|
+
...run,
|
|
254
|
+
lease: normalizedLease,
|
|
255
|
+
updatedAt: now.toISOString(),
|
|
256
|
+
});
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
async startRun(id, lease) {
|
|
260
|
+
const now = this.now();
|
|
261
|
+
const normalizedLease = normalizeScheduleLease(lease, now);
|
|
262
|
+
const run = this.requireOwnedActiveRun(id, normalizedLease, now);
|
|
263
|
+
const schedule = this.requireSchedule(run.scheduleId);
|
|
264
|
+
if (schedule.status !== "active") {
|
|
265
|
+
throw new ScheduleInvalidStateError(`Schedule "${schedule.id}" is not active`);
|
|
266
|
+
}
|
|
267
|
+
if (run.status !== "claimed") {
|
|
268
|
+
throw new ScheduleInvalidStateError(`Schedule run "${id}" must be claimed before it can start`);
|
|
269
|
+
}
|
|
270
|
+
const started = {
|
|
271
|
+
...run,
|
|
272
|
+
status: "running",
|
|
273
|
+
lease: normalizedLease,
|
|
274
|
+
startedAt: run.startedAt ?? now.toISOString(),
|
|
275
|
+
updatedAt: now.toISOString(),
|
|
276
|
+
};
|
|
277
|
+
this.runs.set(id, clone(started));
|
|
278
|
+
return clone(started);
|
|
279
|
+
}
|
|
280
|
+
async releaseRun(id, lease) {
|
|
281
|
+
const now = this.now();
|
|
282
|
+
const normalizedLease = normalizeScheduleLease(lease, now);
|
|
283
|
+
const run = this.requireOwnedActiveRun(id, normalizedLease, now);
|
|
284
|
+
const released = {
|
|
285
|
+
...run,
|
|
286
|
+
status: "pending",
|
|
287
|
+
updatedAt: now.toISOString(),
|
|
288
|
+
};
|
|
289
|
+
delete released.lease;
|
|
290
|
+
this.runs.set(id, clone(released));
|
|
291
|
+
return clone(released);
|
|
292
|
+
}
|
|
293
|
+
async completeRun(id, input) {
|
|
294
|
+
const now = this.now();
|
|
295
|
+
const completion = normalizeCompleteScheduleRunInput(input, now);
|
|
296
|
+
const run = this.requireRun(id);
|
|
297
|
+
if (isTerminalScheduleRunStatus(run.status)) {
|
|
298
|
+
throw new ScheduleConflictError(`Schedule run "${id}" is already terminal`);
|
|
299
|
+
}
|
|
300
|
+
const owned = this.requireOwnedActiveRun(id, completion.lease, now);
|
|
301
|
+
try {
|
|
302
|
+
assertScheduleRunStatusTransition(owned.status, completion.status);
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
throw new ScheduleInvalidStateError(errorMessage(error));
|
|
306
|
+
}
|
|
307
|
+
const completed = {
|
|
308
|
+
...owned,
|
|
309
|
+
status: completion.status,
|
|
310
|
+
references: completion.references,
|
|
311
|
+
...(completion.result === undefined ? {} : { result: completion.result }),
|
|
312
|
+
...(completion.error === undefined ? {} : { error: completion.error }),
|
|
313
|
+
completedAt: now.toISOString(),
|
|
314
|
+
updatedAt: now.toISOString(),
|
|
315
|
+
};
|
|
316
|
+
delete completed.lease;
|
|
317
|
+
this.runs.set(id, clone(completed));
|
|
318
|
+
return clone(completed);
|
|
319
|
+
}
|
|
320
|
+
async countActiveRuns(scheduleId) {
|
|
321
|
+
const now = this.now().getTime();
|
|
322
|
+
return [...this.runs.values()].filter((run) => run.scheduleId === scheduleId
|
|
323
|
+
&& isActiveRun(run)
|
|
324
|
+
&& Boolean(run.lease)
|
|
325
|
+
&& Date.parse(run.lease.expiresAt) > now).length;
|
|
326
|
+
}
|
|
327
|
+
requireSchedule(id) {
|
|
328
|
+
const schedule = this.schedules.get(id);
|
|
329
|
+
if (!schedule)
|
|
330
|
+
throw new ScheduleNotFoundError("Schedule", id);
|
|
331
|
+
return schedule;
|
|
332
|
+
}
|
|
333
|
+
requireRun(id) {
|
|
334
|
+
const run = this.runs.get(id);
|
|
335
|
+
if (!run)
|
|
336
|
+
throw new ScheduleNotFoundError("Schedule run", id);
|
|
337
|
+
return run;
|
|
338
|
+
}
|
|
339
|
+
requireOwnedActiveRun(id, lease, now) {
|
|
340
|
+
const run = this.requireRun(id);
|
|
341
|
+
if (!isActiveRun(run) || !run.lease || !leaseMatches(run.lease, lease)) {
|
|
342
|
+
throw new ScheduleConflictError(`Schedule run "${id}" is not owned by this lease`);
|
|
343
|
+
}
|
|
344
|
+
if (Date.parse(run.lease.expiresAt) <= now.getTime()) {
|
|
345
|
+
throw new ScheduleConflictError(`Schedule run "${id}" lease has expired`);
|
|
346
|
+
}
|
|
347
|
+
return run;
|
|
348
|
+
}
|
|
349
|
+
now() {
|
|
350
|
+
const now = new Date(this.options.now());
|
|
351
|
+
if (!Number.isFinite(now.getTime())) {
|
|
352
|
+
throw new ScheduleInvalidStateError("Schedule store clock returned an invalid date");
|
|
353
|
+
}
|
|
354
|
+
return now;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
export function normalizeCreateScheduleRunInput(input) {
|
|
358
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
359
|
+
throw new Error("Schedule run input must be an object");
|
|
360
|
+
}
|
|
361
|
+
assertOnlyKeys(input, ["id", "scheduleId", "occurrenceAt", "triggerId", "idempotencyKey"], "Schedule run input");
|
|
362
|
+
return {
|
|
363
|
+
...(input.id === undefined ? {} : { id: nonEmpty(input.id, "Schedule run id") }),
|
|
364
|
+
scheduleId: nonEmpty(input.scheduleId, "Schedule run scheduleId"),
|
|
365
|
+
occurrenceAt: absoluteTimestamp(input.occurrenceAt, "Schedule run occurrenceAt"),
|
|
366
|
+
triggerId: nonEmpty(input.triggerId, "Schedule run triggerId"),
|
|
367
|
+
idempotencyKey: nonEmpty(input.idempotencyKey, "Schedule run idempotencyKey"),
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
export function normalizeScheduleLease(value, now, requireFuture = true) {
|
|
371
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
372
|
+
throw new Error("Schedule run lease must be an object");
|
|
373
|
+
}
|
|
374
|
+
assertOnlyKeys(value, ["owner", "token", "expiresAt"], "Schedule run lease");
|
|
375
|
+
const normalized = {
|
|
376
|
+
owner: nonEmpty(value.owner, "Schedule run lease owner"),
|
|
377
|
+
token: nonEmpty(value.token, "Schedule run lease token"),
|
|
378
|
+
expiresAt: absoluteTimestamp(value.expiresAt, "Schedule run lease expiresAt"),
|
|
379
|
+
};
|
|
380
|
+
if (requireFuture && Date.parse(normalized.expiresAt) <= now.getTime()) {
|
|
381
|
+
throw new Error("Schedule run lease expiresAt must be in the future");
|
|
382
|
+
}
|
|
383
|
+
return normalized;
|
|
384
|
+
}
|
|
385
|
+
export function normalizeCompleteScheduleRunInput(value, now) {
|
|
386
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
387
|
+
throw new Error("Schedule run completion must be an object");
|
|
388
|
+
}
|
|
389
|
+
assertOnlyKeys(value, ["lease", "status", "references", "result", "error"], "Schedule run completion");
|
|
390
|
+
if (!["succeeded", "failed", "skipped", "cancelled"].includes(value.status)) {
|
|
391
|
+
throw new Error("Schedule run completion status is invalid");
|
|
392
|
+
}
|
|
393
|
+
const references = normalizeReferences(value.references);
|
|
394
|
+
const result = value.result === undefined
|
|
395
|
+
? undefined
|
|
396
|
+
: normalizeScheduleMetadata(value.result, "Schedule run result");
|
|
397
|
+
const error = value.error === undefined ? undefined : normalizeRunError(value.error);
|
|
398
|
+
if (value.status === "failed" && !error) {
|
|
399
|
+
throw new Error("A failed schedule run completion requires error details");
|
|
400
|
+
}
|
|
401
|
+
if (value.status === "succeeded" && error) {
|
|
402
|
+
throw new Error("A succeeded schedule run completion cannot include an error");
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
lease: normalizeScheduleLease(value.lease, now, false),
|
|
406
|
+
status: value.status,
|
|
407
|
+
references,
|
|
408
|
+
...(result === undefined ? {} : { result }),
|
|
409
|
+
...(error === undefined ? {} : { error }),
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
export function normalizeScheduleOperationalStatePatch(value) {
|
|
413
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
414
|
+
throw new Error("Schedule operational state patch must be an object");
|
|
415
|
+
}
|
|
416
|
+
assertOnlyKeys(value, ["nextOccurrenceAt", "lastOccurrenceAt", "driver"], "Schedule operational state patch");
|
|
417
|
+
if (Object.keys(value).length === 0) {
|
|
418
|
+
throw new Error("Schedule operational state patch must include at least one field");
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
...(value.nextOccurrenceAt === undefined
|
|
422
|
+
? {}
|
|
423
|
+
: {
|
|
424
|
+
nextOccurrenceAt: value.nextOccurrenceAt === null
|
|
425
|
+
? null
|
|
426
|
+
: absoluteTimestamp(value.nextOccurrenceAt, "Schedule nextOccurrenceAt"),
|
|
427
|
+
}),
|
|
428
|
+
...(value.lastOccurrenceAt === undefined
|
|
429
|
+
? {}
|
|
430
|
+
: {
|
|
431
|
+
lastOccurrenceAt: value.lastOccurrenceAt === null
|
|
432
|
+
? null
|
|
433
|
+
: absoluteTimestamp(value.lastOccurrenceAt, "Schedule lastOccurrenceAt"),
|
|
434
|
+
}),
|
|
435
|
+
...(value.driver === undefined
|
|
436
|
+
? {}
|
|
437
|
+
: {
|
|
438
|
+
driver: value.driver === null
|
|
439
|
+
? null
|
|
440
|
+
: normalizeDriverRegistration(value.driver),
|
|
441
|
+
}),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
function normalizeReferences(value) {
|
|
445
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
446
|
+
throw new Error("Schedule run references must be an object");
|
|
447
|
+
}
|
|
448
|
+
const allowed = new Set([
|
|
449
|
+
"runtimeId",
|
|
450
|
+
"taskId",
|
|
451
|
+
"loopRunId",
|
|
452
|
+
"sessionId",
|
|
453
|
+
"channelEventId",
|
|
454
|
+
"providerDeliveryId",
|
|
455
|
+
]);
|
|
456
|
+
const normalized = {};
|
|
457
|
+
for (const [key, child] of Object.entries(value)) {
|
|
458
|
+
if (!allowed.has(key)) {
|
|
459
|
+
throw new Error(`Schedule run references contains unsupported field "${key}"`);
|
|
460
|
+
}
|
|
461
|
+
normalized[key] = nonEmpty(child, `Schedule run reference ${key}`);
|
|
462
|
+
}
|
|
463
|
+
return normalized;
|
|
464
|
+
}
|
|
465
|
+
function normalizeRunError(value) {
|
|
466
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
467
|
+
throw new Error("Schedule run error must be an object");
|
|
468
|
+
}
|
|
469
|
+
assertOnlyKeys(value, ["code", "message", "retryable", "metadata"], "Schedule run error");
|
|
470
|
+
if (typeof value.retryable !== "boolean") {
|
|
471
|
+
throw new Error("Schedule run error retryable must be a boolean");
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
code: nonEmpty(value.code, "Schedule run error code"),
|
|
475
|
+
message: nonEmpty(value.message, "Schedule run error message"),
|
|
476
|
+
retryable: value.retryable,
|
|
477
|
+
...(value.metadata === undefined
|
|
478
|
+
? {}
|
|
479
|
+
: {
|
|
480
|
+
metadata: normalizeScheduleMetadata(value.metadata, "Schedule run error metadata"),
|
|
481
|
+
}),
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
function normalizeDriverRegistration(value) {
|
|
485
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
486
|
+
throw new Error("Schedule driver registration must be an object");
|
|
487
|
+
}
|
|
488
|
+
assertOnlyKeys(value, ["kind", "status", "providerId", "metadata", "error", "updatedAt"], "Schedule driver registration");
|
|
489
|
+
if (!["pending", "registered", "failed", "not_required"].includes(value.status)) {
|
|
490
|
+
throw new Error("Schedule driver registration status is invalid");
|
|
491
|
+
}
|
|
492
|
+
if (value.status === "registered" && value.providerId === undefined) {
|
|
493
|
+
throw new Error("A registered schedule driver requires providerId");
|
|
494
|
+
}
|
|
495
|
+
if (value.status === "failed" && value.error === undefined) {
|
|
496
|
+
throw new Error("A failed schedule driver registration requires error details");
|
|
497
|
+
}
|
|
498
|
+
if (value.status !== "failed" && value.error !== undefined) {
|
|
499
|
+
throw new Error("Only a failed schedule driver registration may include an error");
|
|
500
|
+
}
|
|
501
|
+
return {
|
|
502
|
+
kind: nonEmpty(value.kind, "Schedule driver kind"),
|
|
503
|
+
status: value.status,
|
|
504
|
+
...(value.providerId === undefined
|
|
505
|
+
? {}
|
|
506
|
+
: { providerId: nonEmpty(value.providerId, "Schedule driver providerId") }),
|
|
507
|
+
...(value.metadata === undefined
|
|
508
|
+
? {}
|
|
509
|
+
: {
|
|
510
|
+
metadata: normalizeScheduleMetadata(value.metadata, "Schedule driver metadata"),
|
|
511
|
+
}),
|
|
512
|
+
...(value.error === undefined ? {} : { error: normalizeDriverError(value.error) }),
|
|
513
|
+
updatedAt: absoluteTimestamp(value.updatedAt, "Schedule driver updatedAt"),
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
function normalizeDriverError(value) {
|
|
517
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
518
|
+
throw new Error("Schedule driver error must be an object");
|
|
519
|
+
}
|
|
520
|
+
assertOnlyKeys(value, ["code", "message", "retryable"], "Schedule driver error");
|
|
521
|
+
if (typeof value.retryable !== "boolean") {
|
|
522
|
+
throw new Error("Schedule driver error retryable must be a boolean");
|
|
523
|
+
}
|
|
524
|
+
return {
|
|
525
|
+
code: nonEmpty(value.code, "Schedule driver error code"),
|
|
526
|
+
message: nonEmpty(value.message, "Schedule driver error message"),
|
|
527
|
+
retryable: value.retryable,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
export function assertSameScheduleOccurrence(existing, input) {
|
|
531
|
+
if (existing.scheduleId !== input.scheduleId
|
|
532
|
+
|| existing.occurrenceAt !== input.occurrenceAt
|
|
533
|
+
|| existing.triggerId !== input.triggerId) {
|
|
534
|
+
throw new ScheduleConflictError(`Schedule run idempotency key "${input.idempotencyKey}" is already used by another occurrence`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
function assertRevision(schedule, expected) {
|
|
538
|
+
if (expected === undefined)
|
|
539
|
+
return;
|
|
540
|
+
if (!Number.isInteger(expected) || expected < 1) {
|
|
541
|
+
throw new Error("Schedule expectedRevision must be a positive integer");
|
|
542
|
+
}
|
|
543
|
+
if (schedule.revision !== expected) {
|
|
544
|
+
throw new ScheduleConflictError(`Schedule "${schedule.id}" revision conflict: expected ${expected}, found ${schedule.revision}`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
function applyNullableStringPatch(schedule, key, value) {
|
|
548
|
+
if (value === undefined)
|
|
549
|
+
return;
|
|
550
|
+
if (value === null) {
|
|
551
|
+
delete schedule[key];
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
schedule[key] = value;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
function applyNullableField(schedule, key, value) {
|
|
558
|
+
if (value === undefined)
|
|
559
|
+
return;
|
|
560
|
+
if (value === null)
|
|
561
|
+
delete schedule[key];
|
|
562
|
+
else
|
|
563
|
+
schedule[key] = value;
|
|
564
|
+
}
|
|
565
|
+
function normalizeStatusFilter(status) {
|
|
566
|
+
if (status === undefined)
|
|
567
|
+
return undefined;
|
|
568
|
+
return new Set(Array.isArray(status) ? status : [status]);
|
|
569
|
+
}
|
|
570
|
+
function normalizeRunStatusFilter(status) {
|
|
571
|
+
if (status === undefined)
|
|
572
|
+
return undefined;
|
|
573
|
+
return new Set(Array.isArray(status) ? status : [status]);
|
|
574
|
+
}
|
|
575
|
+
function normalizeLimit(limit) {
|
|
576
|
+
if (limit === undefined)
|
|
577
|
+
return Number.POSITIVE_INFINITY;
|
|
578
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 1_000) {
|
|
579
|
+
throw new Error("Schedule run list limit must be an integer between 1 and 1000");
|
|
580
|
+
}
|
|
581
|
+
return limit;
|
|
582
|
+
}
|
|
583
|
+
function normalizeRunOrder(order) {
|
|
584
|
+
if (order === undefined || order === "desc")
|
|
585
|
+
return -1;
|
|
586
|
+
if (order === "asc")
|
|
587
|
+
return 1;
|
|
588
|
+
throw new Error('Schedule run order must be "asc" or "desc"');
|
|
589
|
+
}
|
|
590
|
+
function isActiveRun(run) {
|
|
591
|
+
return run.status === "claimed" || run.status === "running";
|
|
592
|
+
}
|
|
593
|
+
function leaseMatches(current, candidate) {
|
|
594
|
+
return Boolean(current
|
|
595
|
+
&& current.owner === candidate.owner
|
|
596
|
+
&& current.token === candidate.token);
|
|
597
|
+
}
|
|
598
|
+
function compareCreated(a, b) {
|
|
599
|
+
return a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id);
|
|
600
|
+
}
|
|
601
|
+
function absoluteTimestamp(value, label) {
|
|
602
|
+
const text = nonEmpty(value, label);
|
|
603
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/.test(text)) {
|
|
604
|
+
throw new Error(`${label} must be an absolute ISO timestamp`);
|
|
605
|
+
}
|
|
606
|
+
const date = new Date(text);
|
|
607
|
+
if (!Number.isFinite(date.getTime())) {
|
|
608
|
+
throw new Error(`${label} must be a valid timestamp`);
|
|
609
|
+
}
|
|
610
|
+
return date.toISOString();
|
|
611
|
+
}
|
|
612
|
+
function nonEmpty(value, label) {
|
|
613
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
614
|
+
throw new Error(`${label} must be a non-empty string`);
|
|
615
|
+
}
|
|
616
|
+
if (value.trim().length > 512) {
|
|
617
|
+
throw new Error(`${label} exceeds the 512-character limit`);
|
|
618
|
+
}
|
|
619
|
+
return value.trim();
|
|
620
|
+
}
|
|
621
|
+
function clone(value) {
|
|
622
|
+
return structuredClone(value);
|
|
623
|
+
}
|
|
624
|
+
function errorMessage(error) {
|
|
625
|
+
return error instanceof Error ? error.message : String(error);
|
|
626
|
+
}
|
|
627
|
+
function assertOnlyKeys(value, allowed, label) {
|
|
628
|
+
const allowedSet = new Set(allowed);
|
|
629
|
+
for (const key of Object.keys(value)) {
|
|
630
|
+
if (!allowedSet.has(key)) {
|
|
631
|
+
throw new Error(`${label} contains unsupported field "${key}"`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
//# sourceMappingURL=store.js.map
|