@dudousxd/nestjs-durable-store-typeorm 0.18.0 → 0.19.0
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.cjs +243 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -2
- package/dist/index.d.ts +27 -2
- package/dist/index.js +243 -3
- package/dist/index.js.map +1 -1
- package/package.json +14 -4
package/dist/index.cjs
CHANGED
|
@@ -26,6 +26,7 @@ __export(index_exports, {
|
|
|
26
26
|
ENTITIES: () => ENTITIES,
|
|
27
27
|
JSON_BLOB_COLUMNS: () => JSON_BLOB_COLUMNS,
|
|
28
28
|
RunAttributeEntity: () => RunAttributeEntity,
|
|
29
|
+
ScheduleEntity: () => ScheduleEntity,
|
|
29
30
|
SignalWaiterEntity: () => SignalWaiterEntity,
|
|
30
31
|
StepCheckpointEntity: () => StepCheckpointEntity,
|
|
31
32
|
TypeOrmStateStore: () => TypeOrmStateStore,
|
|
@@ -152,6 +153,23 @@ var BufferedEventEntity = class {
|
|
|
152
153
|
payload;
|
|
153
154
|
publishedAt;
|
|
154
155
|
};
|
|
156
|
+
var ScheduleEntity = class {
|
|
157
|
+
static {
|
|
158
|
+
__name(this, "ScheduleEntity");
|
|
159
|
+
}
|
|
160
|
+
id;
|
|
161
|
+
namespace;
|
|
162
|
+
workflow;
|
|
163
|
+
paused;
|
|
164
|
+
/** Epoch ms as a `bigint` (drivers hand it back as a string) — an exact integer, so the
|
|
165
|
+
* compare-and-set in `updateSchedule` can't be defeated by a DATETIME's truncated milliseconds. */
|
|
166
|
+
nextFireAt;
|
|
167
|
+
tags;
|
|
168
|
+
spec;
|
|
169
|
+
state;
|
|
170
|
+
createdAt;
|
|
171
|
+
updatedAt;
|
|
172
|
+
};
|
|
155
173
|
function durableEntities(options = {}) {
|
|
156
174
|
const col = columnNamer(options.naming ?? "snake_case");
|
|
157
175
|
const workflowRuns = new import_typeorm.EntitySchema({
|
|
@@ -479,13 +497,69 @@ function durableEntities(options = {}) {
|
|
|
479
497
|
}
|
|
480
498
|
}
|
|
481
499
|
});
|
|
500
|
+
const schedules = new import_typeorm.EntitySchema({
|
|
501
|
+
name: "ScheduleEntity",
|
|
502
|
+
target: ScheduleEntity,
|
|
503
|
+
tableName: "durable_schedules",
|
|
504
|
+
columns: {
|
|
505
|
+
id: {
|
|
506
|
+
type: "text",
|
|
507
|
+
primary: true,
|
|
508
|
+
name: col("id")
|
|
509
|
+
},
|
|
510
|
+
namespace: {
|
|
511
|
+
type: "text",
|
|
512
|
+
name: col("namespace"),
|
|
513
|
+
default: "default"
|
|
514
|
+
},
|
|
515
|
+
workflow: {
|
|
516
|
+
type: "text",
|
|
517
|
+
name: col("workflow")
|
|
518
|
+
},
|
|
519
|
+
paused: {
|
|
520
|
+
type: Boolean,
|
|
521
|
+
name: col("paused"),
|
|
522
|
+
default: false
|
|
523
|
+
},
|
|
524
|
+
nextFireAt: {
|
|
525
|
+
type: "bigint",
|
|
526
|
+
nullable: true,
|
|
527
|
+
name: col("nextFireAt")
|
|
528
|
+
},
|
|
529
|
+
tags: {
|
|
530
|
+
type: "text",
|
|
531
|
+
nullable: true,
|
|
532
|
+
name: col("tags"),
|
|
533
|
+
transformer: jsonColumnTransformer("schedules.tags")
|
|
534
|
+
},
|
|
535
|
+
spec: {
|
|
536
|
+
type: "text",
|
|
537
|
+
name: col("spec"),
|
|
538
|
+
transformer: jsonColumnTransformer("schedules.spec")
|
|
539
|
+
},
|
|
540
|
+
state: {
|
|
541
|
+
type: "text",
|
|
542
|
+
name: col("state"),
|
|
543
|
+
transformer: jsonColumnTransformer("schedules.state")
|
|
544
|
+
},
|
|
545
|
+
createdAt: {
|
|
546
|
+
type: Date,
|
|
547
|
+
name: col("createdAt")
|
|
548
|
+
},
|
|
549
|
+
updatedAt: {
|
|
550
|
+
type: Date,
|
|
551
|
+
name: col("updatedAt")
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
});
|
|
482
555
|
return [
|
|
483
556
|
workflowRuns,
|
|
484
557
|
stepCheckpoints,
|
|
485
558
|
runAttributes,
|
|
486
559
|
signalWaiters,
|
|
487
560
|
bufferedSignals,
|
|
488
|
-
bufferedEvents
|
|
561
|
+
bufferedEvents,
|
|
562
|
+
schedules
|
|
489
563
|
];
|
|
490
564
|
}
|
|
491
565
|
__name(durableEntities, "durableEntities");
|
|
@@ -513,7 +587,8 @@ var TABLE_TO_ENTITY = {
|
|
|
513
587
|
durable_run_attributes: RunAttributeEntity,
|
|
514
588
|
durable_signal_waiters: SignalWaiterEntity,
|
|
515
589
|
durable_buffered_signals: BufferedSignalEntity,
|
|
516
|
-
durable_buffered_events: BufferedEventEntity
|
|
590
|
+
durable_buffered_events: BufferedEventEntity,
|
|
591
|
+
durable_schedules: ScheduleEntity
|
|
517
592
|
};
|
|
518
593
|
function durableManagedTables() {
|
|
519
594
|
return Object.keys(TABLE_TO_ENTITY);
|
|
@@ -571,6 +646,8 @@ async function ensureTypeOrmDurableSchema(dataSource) {
|
|
|
571
646
|
const waiterCol = /* @__PURE__ */ __name((property) => q(resolve("durable_signal_waiters", property)), "waiterCol");
|
|
572
647
|
const bufCol = /* @__PURE__ */ __name((property) => q(resolve("durable_buffered_signals", property)), "bufCol");
|
|
573
648
|
const bevCol = /* @__PURE__ */ __name((property) => q(resolve("durable_buffered_events", property)), "bevCol");
|
|
649
|
+
const schedules = q("durable_schedules");
|
|
650
|
+
const schCol = /* @__PURE__ */ __name((property) => q(resolve("durable_schedules", property)), "schCol");
|
|
574
651
|
const num = isPg ? "double precision" : isMysql ? "double" : "real";
|
|
575
652
|
const bufferedId = isSqlite ? `${bufCol("id")} integer PRIMARY KEY AUTOINCREMENT` : isMysql ? `${bufCol("id")} bigint NOT NULL AUTO_INCREMENT PRIMARY KEY` : isPg ? `${bufCol("id")} bigserial PRIMARY KEY` : `${bufCol("id")} bigint NOT NULL AUTO_INCREMENT PRIMARY KEY`;
|
|
576
653
|
const tables = [
|
|
@@ -614,6 +691,16 @@ async function ensureTypeOrmDurableSchema(dataSource) {
|
|
|
614
691
|
`CREATE TABLE IF NOT EXISTS ${bufferedEvents} (
|
|
615
692
|
${bevCol("id")} ${str} PRIMARY KEY, ${bevCol("name")} ${str} NOT NULL,
|
|
616
693
|
${bevCol("payload")} ${txt}, ${bevCol("publishedAt")} ${ts} NOT NULL
|
|
694
|
+
)`,
|
|
695
|
+
// Persisted schedules. `next_fire_at` is epoch ms as a bigint (exact, so the compare-and-set on
|
|
696
|
+
// it survives MySQL's DATETIME millisecond truncation); `spec`/`state` are engine-owned JSON.
|
|
697
|
+
`CREATE TABLE IF NOT EXISTS ${schedules} (
|
|
698
|
+
${schCol("id")} ${str} PRIMARY KEY, ${schCol("namespace")} ${nsCol},
|
|
699
|
+
${schCol("workflow")} ${str} NOT NULL,
|
|
700
|
+
${schCol("paused")} boolean NOT NULL DEFAULT ${isPg ? "false" : "0"},
|
|
701
|
+
${schCol("nextFireAt")} bigint, ${schCol("tags")} ${txt},
|
|
702
|
+
${schCol("spec")} ${txt} NOT NULL, ${schCol("state")} ${txt} NOT NULL,
|
|
703
|
+
${schCol("createdAt")} ${ts} NOT NULL, ${schCol("updatedAt")} ${ts} NOT NULL
|
|
617
704
|
)`
|
|
618
705
|
];
|
|
619
706
|
const additive = {
|
|
@@ -757,6 +844,11 @@ async function ensureTypeOrmDurableSchema(dataSource) {
|
|
|
757
844
|
],
|
|
758
845
|
// Search-attribute pushdown: equality + range predicates probe by (key, value). Two composite
|
|
759
846
|
// indexes — one per typed column — so a numeric range or a string equality is an index scan.
|
|
847
|
+
// Persisted schedules: the poll's "due by T" scan (`paused = false AND next_fire_at <= T`).
|
|
848
|
+
[
|
|
849
|
+
"durable_schedules_due_idx",
|
|
850
|
+
`${schedules} (${schCol("paused")}, ${schCol("nextFireAt")})`
|
|
851
|
+
],
|
|
760
852
|
[
|
|
761
853
|
"durable_run_attributes_num_idx",
|
|
762
854
|
`${runAttributes} (${attrCol("key")}, ${attrCol("numValue")})`
|
|
@@ -823,6 +915,9 @@ var TypeOrmStateStore = class {
|
|
|
823
915
|
attributes() {
|
|
824
916
|
return this.dataSource.getRepository(RunAttributeEntity);
|
|
825
917
|
}
|
|
918
|
+
schedules() {
|
|
919
|
+
return this.dataSource.getRepository(ScheduleEntity);
|
|
920
|
+
}
|
|
826
921
|
/** Rewrite a run's normalized attribute rows: delete the old set, insert the current one. Mirrors
|
|
827
922
|
* the in-memory store's reindex so the side-table always reflects the run's live searchAttributes. */
|
|
828
923
|
async reindexAttributes(runId, attributes, em) {
|
|
@@ -883,6 +978,52 @@ var TypeOrmStateStore = class {
|
|
|
883
978
|
id: runId
|
|
884
979
|
});
|
|
885
980
|
}
|
|
981
|
+
async deleteRuns(runIds) {
|
|
982
|
+
if (runIds.length === 0) return;
|
|
983
|
+
await this.dataSource.transaction(async (em) => {
|
|
984
|
+
await em.getRepository(StepCheckpointEntity).delete({
|
|
985
|
+
runId: (0, import_typeorm2.In)(runIds)
|
|
986
|
+
});
|
|
987
|
+
await em.getRepository(SignalWaiterEntity).delete({
|
|
988
|
+
runId: (0, import_typeorm2.In)(runIds)
|
|
989
|
+
});
|
|
990
|
+
await em.getRepository(RunAttributeEntity).delete({
|
|
991
|
+
runId: (0, import_typeorm2.In)(runIds)
|
|
992
|
+
});
|
|
993
|
+
await em.getRepository(WorkflowRunEntity).delete({
|
|
994
|
+
id: (0, import_typeorm2.In)(runIds)
|
|
995
|
+
});
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
async pruneTerminalRuns(policy, nowMs, limit) {
|
|
999
|
+
const statuses = policy.statuses.filter((s) => import_nestjs_durable_core.TERMINAL_RUN_STATUSES.includes(s));
|
|
1000
|
+
if (statuses.length === 0 || limit <= 0) return 0;
|
|
1001
|
+
if (policy.maxAge == null && policy.maxCount == null) return 0;
|
|
1002
|
+
const scoped = /* @__PURE__ */ __name(() => this.runQueryBuilder({
|
|
1003
|
+
...policy.scope ?? {},
|
|
1004
|
+
statuses
|
|
1005
|
+
}).select("r.id", "id"), "scoped");
|
|
1006
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1007
|
+
if (policy.maxAge != null) {
|
|
1008
|
+
const cutoff = new Date(nowMs - (0, import_nestjs_durable_core.parseDuration)(policy.maxAge));
|
|
1009
|
+
const rows = await scoped().andWhere("r.updatedAt < :cutoff", {
|
|
1010
|
+
cutoff
|
|
1011
|
+
}).orderBy("r.updatedAt", "ASC").limit(limit).getRawMany();
|
|
1012
|
+
for (const r of rows) ids.add(r.id);
|
|
1013
|
+
}
|
|
1014
|
+
if (policy.maxCount != null && ids.size < limit) {
|
|
1015
|
+
const rows = await scoped().orderBy("r.updatedAt", "DESC").addOrderBy("r.id", "DESC").limit(limit).offset(policy.maxCount).getRawMany();
|
|
1016
|
+
for (const r of rows) {
|
|
1017
|
+
if (ids.size >= limit) break;
|
|
1018
|
+
ids.add(r.id);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
const doomed = [
|
|
1022
|
+
...ids
|
|
1023
|
+
].slice(0, limit);
|
|
1024
|
+
await this.deleteRuns(doomed);
|
|
1025
|
+
return doomed.length;
|
|
1026
|
+
}
|
|
886
1027
|
async getCheckpoint(runId, seq) {
|
|
887
1028
|
const e = await this.checkpoints().findOneBy({
|
|
888
1029
|
runId,
|
|
@@ -1038,6 +1179,9 @@ var TypeOrmStateStore = class {
|
|
|
1038
1179
|
const rows = await qb.getMany();
|
|
1039
1180
|
return rows.map(fromRunEntity);
|
|
1040
1181
|
}
|
|
1182
|
+
async countRuns(query) {
|
|
1183
|
+
return this.runQueryBuilder(query).getCount();
|
|
1184
|
+
}
|
|
1041
1185
|
/** `GROUP BY status, origin` over the same predicates {@link listRuns} pages — one aggregate, so a
|
|
1042
1186
|
* console can show whole-set counts next to a bounded page instead of downloading every run to
|
|
1043
1187
|
* count them in the browser. */
|
|
@@ -1100,6 +1244,69 @@ var TypeOrmStateStore = class {
|
|
|
1100
1244
|
const type = String(this.dataSource.options.type);
|
|
1101
1245
|
return type === "mysql" || type === "mariadb" || type === "aurora-mysql" ? "`" : '"';
|
|
1102
1246
|
}
|
|
1247
|
+
// ---- persisted schedules ----------------------------------------------------------------------
|
|
1248
|
+
async saveSchedule(record) {
|
|
1249
|
+
await this.schedules().save(toScheduleEntity(record));
|
|
1250
|
+
}
|
|
1251
|
+
async getSchedule(id) {
|
|
1252
|
+
const e = await this.schedules().findOneBy({
|
|
1253
|
+
id
|
|
1254
|
+
});
|
|
1255
|
+
return e ? fromScheduleEntity(e) : null;
|
|
1256
|
+
}
|
|
1257
|
+
/** One conditional UPDATE: the compare-and-set on `next_fire_at` (an exact bigint) is atomic. */
|
|
1258
|
+
async updateSchedule(id, patch, expectedNextFireAt) {
|
|
1259
|
+
const set = {};
|
|
1260
|
+
if (patch.namespace !== void 0) set.namespace = patch.namespace;
|
|
1261
|
+
if (patch.workflow !== void 0) set.workflow = patch.workflow;
|
|
1262
|
+
if (patch.paused !== void 0) set.paused = patch.paused;
|
|
1263
|
+
if ("nextFireAt" in patch) set.nextFireAt = patch.nextFireAt ?? null;
|
|
1264
|
+
if ("tags" in patch) set.tags = patch.tags ?? null;
|
|
1265
|
+
if (patch.spec !== void 0) set.spec = patch.spec;
|
|
1266
|
+
if (patch.state !== void 0) set.state = patch.state;
|
|
1267
|
+
if (patch.updatedAt !== void 0) set.updatedAt = patch.updatedAt;
|
|
1268
|
+
if (!Object.keys(set).length) return await this.getSchedule(id) !== null;
|
|
1269
|
+
const qb = this.schedules().createQueryBuilder().update().set(set).where({
|
|
1270
|
+
id
|
|
1271
|
+
});
|
|
1272
|
+
if (expectedNextFireAt === null) qb.andWhere({
|
|
1273
|
+
nextFireAt: (0, import_typeorm2.IsNull)()
|
|
1274
|
+
});
|
|
1275
|
+
else if (expectedNextFireAt !== void 0) qb.andWhere({
|
|
1276
|
+
nextFireAt: expectedNextFireAt
|
|
1277
|
+
});
|
|
1278
|
+
const result = await qb.execute();
|
|
1279
|
+
return (result.affected ?? 0) > 0;
|
|
1280
|
+
}
|
|
1281
|
+
async deleteSchedule(id) {
|
|
1282
|
+
const result = await this.schedules().delete({
|
|
1283
|
+
id
|
|
1284
|
+
});
|
|
1285
|
+
return (result.affected ?? 0) > 0;
|
|
1286
|
+
}
|
|
1287
|
+
async listSchedules(query) {
|
|
1288
|
+
const qb = this.schedules().createQueryBuilder("s");
|
|
1289
|
+
if (query.namespace !== void 0) qb.andWhere("s.namespace = :namespace", {
|
|
1290
|
+
namespace: query.namespace
|
|
1291
|
+
});
|
|
1292
|
+
if (query.workflow !== void 0) qb.andWhere("s.workflow = :workflow", {
|
|
1293
|
+
workflow: query.workflow
|
|
1294
|
+
});
|
|
1295
|
+
if (query.paused !== void 0) qb.andWhere("s.paused = :paused", {
|
|
1296
|
+
paused: query.paused
|
|
1297
|
+
});
|
|
1298
|
+
if (query.tag !== void 0) qb.andWhere("s.tags LIKE :tag", {
|
|
1299
|
+
tag: `%"${query.tag}"%`
|
|
1300
|
+
});
|
|
1301
|
+
if (query.dueBy !== void 0) qb.andWhere("s.paused = :notPaused AND s.nextFireAt <= :dueBy", {
|
|
1302
|
+
notPaused: false,
|
|
1303
|
+
dueBy: query.dueBy
|
|
1304
|
+
});
|
|
1305
|
+
qb.orderBy("CASE WHEN s.nextFireAt IS NULL THEN 1 ELSE 0 END", "ASC").addOrderBy("s.nextFireAt", "ASC").addOrderBy("s.id", "ASC");
|
|
1306
|
+
if (query.limit != null) qb.take(query.limit);
|
|
1307
|
+
if (query.offset != null) qb.skip(query.offset);
|
|
1308
|
+
return (await qb.getMany()).map(fromScheduleEntity);
|
|
1309
|
+
}
|
|
1103
1310
|
async listCheckpoints(runId) {
|
|
1104
1311
|
const rows = await this.checkpoints().find({
|
|
1105
1312
|
where: {
|
|
@@ -1343,6 +1550,39 @@ function fromCheckpointEntity(e) {
|
|
|
1343
1550
|
};
|
|
1344
1551
|
}
|
|
1345
1552
|
__name(fromCheckpointEntity, "fromCheckpointEntity");
|
|
1553
|
+
function toScheduleEntity(r) {
|
|
1554
|
+
const e = new ScheduleEntity();
|
|
1555
|
+
e.id = r.id;
|
|
1556
|
+
e.namespace = r.namespace;
|
|
1557
|
+
e.workflow = r.workflow;
|
|
1558
|
+
e.paused = r.paused;
|
|
1559
|
+
e.nextFireAt = r.nextFireAt;
|
|
1560
|
+
e.tags = r.tags ?? null;
|
|
1561
|
+
e.spec = r.spec;
|
|
1562
|
+
e.state = r.state;
|
|
1563
|
+
e.createdAt = r.createdAt;
|
|
1564
|
+
e.updatedAt = r.updatedAt;
|
|
1565
|
+
return e;
|
|
1566
|
+
}
|
|
1567
|
+
__name(toScheduleEntity, "toScheduleEntity");
|
|
1568
|
+
function fromScheduleEntity(e) {
|
|
1569
|
+
return {
|
|
1570
|
+
id: e.id,
|
|
1571
|
+
namespace: e.namespace,
|
|
1572
|
+
workflow: e.workflow,
|
|
1573
|
+
// SQLite/MySQL hand a boolean column back as 0/1.
|
|
1574
|
+
paused: Boolean(e.paused),
|
|
1575
|
+
nextFireAt: e.nextFireAt == null ? null : Number(e.nextFireAt),
|
|
1576
|
+
...e.tags ? {
|
|
1577
|
+
tags: e.tags
|
|
1578
|
+
} : {},
|
|
1579
|
+
spec: e.spec ?? {},
|
|
1580
|
+
state: e.state ?? {},
|
|
1581
|
+
createdAt: e.createdAt,
|
|
1582
|
+
updatedAt: e.updatedAt
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
__name(fromScheduleEntity, "fromScheduleEntity");
|
|
1346
1586
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1347
1587
|
0 && (module.exports = {
|
|
1348
1588
|
BufferedEventEntity,
|
|
@@ -1350,6 +1590,7 @@ __name(fromCheckpointEntity, "fromCheckpointEntity");
|
|
|
1350
1590
|
ENTITIES,
|
|
1351
1591
|
JSON_BLOB_COLUMNS,
|
|
1352
1592
|
RunAttributeEntity,
|
|
1593
|
+
ScheduleEntity,
|
|
1353
1594
|
SignalWaiterEntity,
|
|
1354
1595
|
StepCheckpointEntity,
|
|
1355
1596
|
TypeOrmStateStore,
|