@neofinancial/chrono-mongo-datastore 0.5.1 → 0.5.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/build/{index.js → index.cjs} +11 -36
- package/build/index.cjs.map +1 -0
- package/build/{index.d.ts → index.d.cts} +18 -18
- package/build/index.d.mts +17 -17
- package/build/index.mjs.map +1 -1
- package/package.json +15 -14
- package/build/index.js.map +0 -1
|
@@ -1,30 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __copyProps = (to, from, except, desc) => {
|
|
9
|
-
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
-
key = keys[i];
|
|
11
|
-
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
-
get: ((k) => from[k]).bind(null, key),
|
|
13
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
-
value: mod,
|
|
20
|
-
enumerable: true
|
|
21
|
-
}) : target, mod));
|
|
22
|
-
|
|
23
|
-
//#endregion
|
|
24
|
-
let __neofinancial_chrono = require("@neofinancial/chrono");
|
|
25
|
-
__neofinancial_chrono = __toESM(__neofinancial_chrono);
|
|
1
|
+
let _neofinancial_chrono = require("@neofinancial/chrono");
|
|
26
2
|
let mongodb = require("mongodb");
|
|
27
|
-
mongodb = __toESM(mongodb);
|
|
28
3
|
|
|
29
4
|
//#region src/mongo-indexes.ts
|
|
30
5
|
const DEFAULT_EXPIRY_SECONDS = 3600 * 24 * 30;
|
|
@@ -37,7 +12,7 @@ async function ensureIndexes(collection, options) {
|
|
|
37
12
|
await collection.createIndex({ completedAt: -1 }, {
|
|
38
13
|
partialFilterExpression: {
|
|
39
14
|
completedAt: { $exists: true },
|
|
40
|
-
status: { $eq:
|
|
15
|
+
status: { $eq: _neofinancial_chrono.TaskStatus.COMPLETED }
|
|
41
16
|
},
|
|
42
17
|
expireAfterSeconds: options.expireAfterSeconds || DEFAULT_EXPIRY_SECONDS,
|
|
43
18
|
name: IndexNames.COMPLETED_DOCUMENT_TTL_INDEX
|
|
@@ -95,7 +70,7 @@ var ChronoMongoDatastore = class {
|
|
|
95
70
|
async schedule(input) {
|
|
96
71
|
const createInput = {
|
|
97
72
|
kind: input.kind,
|
|
98
|
-
status:
|
|
73
|
+
status: _neofinancial_chrono.TaskStatus.PENDING,
|
|
99
74
|
data: input.data,
|
|
100
75
|
priority: input.priority,
|
|
101
76
|
idempotencyKey: input.idempotencyKey,
|
|
@@ -132,7 +107,7 @@ var ChronoMongoDatastore = class {
|
|
|
132
107
|
};
|
|
133
108
|
const task = await (await this.collection()).findOneAndDelete({
|
|
134
109
|
...filter,
|
|
135
|
-
...options?.force ? {} : { status:
|
|
110
|
+
...options?.force ? {} : { status: _neofinancial_chrono.TaskStatus.PENDING }
|
|
136
111
|
});
|
|
137
112
|
if (!task) {
|
|
138
113
|
if (options?.force) return;
|
|
@@ -146,12 +121,12 @@ var ChronoMongoDatastore = class {
|
|
|
146
121
|
const task = await (await this.collection()).findOneAndUpdate({
|
|
147
122
|
kind: input.kind,
|
|
148
123
|
scheduledAt: { $lte: now },
|
|
149
|
-
$or: [{ status:
|
|
150
|
-
status:
|
|
124
|
+
$or: [{ status: _neofinancial_chrono.TaskStatus.PENDING }, {
|
|
125
|
+
status: _neofinancial_chrono.TaskStatus.CLAIMED,
|
|
151
126
|
claimedAt: { $lte: new Date(now.getTime() - input.claimStaleTimeoutMs) }
|
|
152
127
|
}]
|
|
153
128
|
}, { $set: {
|
|
154
|
-
status:
|
|
129
|
+
status: _neofinancial_chrono.TaskStatus.CLAIMED,
|
|
155
130
|
claimedAt: now
|
|
156
131
|
} }, {
|
|
157
132
|
sort: {
|
|
@@ -165,7 +140,7 @@ var ChronoMongoDatastore = class {
|
|
|
165
140
|
async retry(taskId, retryAt) {
|
|
166
141
|
const taskDocument = await this.updateOrThrow(taskId, {
|
|
167
142
|
$set: {
|
|
168
|
-
status:
|
|
143
|
+
status: _neofinancial_chrono.TaskStatus.PENDING,
|
|
169
144
|
scheduledAt: retryAt
|
|
170
145
|
},
|
|
171
146
|
$inc: { retryCount: 1 }
|
|
@@ -175,7 +150,7 @@ var ChronoMongoDatastore = class {
|
|
|
175
150
|
async complete(taskId) {
|
|
176
151
|
const now = /* @__PURE__ */ new Date();
|
|
177
152
|
const task = await this.updateOrThrow(taskId, { $set: {
|
|
178
|
-
status:
|
|
153
|
+
status: _neofinancial_chrono.TaskStatus.COMPLETED,
|
|
179
154
|
completedAt: now,
|
|
180
155
|
lastExecutedAt: now
|
|
181
156
|
} });
|
|
@@ -184,7 +159,7 @@ var ChronoMongoDatastore = class {
|
|
|
184
159
|
async fail(taskId) {
|
|
185
160
|
const now = /* @__PURE__ */ new Date();
|
|
186
161
|
const task = await this.updateOrThrow(taskId, { $set: {
|
|
187
|
-
status:
|
|
162
|
+
status: _neofinancial_chrono.TaskStatus.FAILED,
|
|
188
163
|
lastExecutedAt: now
|
|
189
164
|
} });
|
|
190
165
|
return this.toObject(task);
|
|
@@ -216,4 +191,4 @@ var ChronoMongoDatastore = class {
|
|
|
216
191
|
|
|
217
192
|
//#endregion
|
|
218
193
|
exports.ChronoMongoDatastore = ChronoMongoDatastore;
|
|
219
|
-
//# sourceMappingURL=index.
|
|
194
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["TaskStatus","TaskStatus","ObjectId"],"sources":["../src/mongo-indexes.ts","../src/chrono-mongo-datastore.ts"],"sourcesContent":["import { TaskStatus } from '@neofinancial/chrono';\nimport type { Collection } from 'mongodb';\n\nexport const DEFAULT_EXPIRY_SECONDS: number = 60 * 60 * 24 * 30; // 30 days\n\nexport const IndexNames = {\n COMPLETED_DOCUMENT_TTL_INDEX: 'chrono-completed-document-ttl-index',\n CLAIM_DOCUMENT_INDEX: 'chrono-claim-document-index',\n IDEMPOTENCY_KEY_INDEX: 'chrono-idempotency-key-index',\n};\n\nexport type IndexDefinitionOptions = {\n expireAfterSeconds?: number;\n};\n\nexport async function ensureIndexes(collection: Collection, options: IndexDefinitionOptions): Promise<void> {\n await collection.createIndex(\n { completedAt: -1 },\n {\n partialFilterExpression: {\n completedAt: { $exists: true },\n status: { $eq: TaskStatus.COMPLETED },\n },\n expireAfterSeconds: options.expireAfterSeconds || DEFAULT_EXPIRY_SECONDS,\n name: IndexNames.COMPLETED_DOCUMENT_TTL_INDEX,\n },\n );\n\n await collection.createIndex(\n { kind: 1, status: 1, scheduledAt: 1, priority: -1, claimedAt: 1 },\n { name: IndexNames.CLAIM_DOCUMENT_INDEX },\n );\n\n await collection.createIndex(\n { idempotencyKey: 1 },\n { name: IndexNames.IDEMPOTENCY_KEY_INDEX, unique: true, sparse: true },\n );\n}\n","import {\n type ClaimTaskInput,\n type Datastore,\n type DeleteInput,\n type DeleteOptions,\n type ScheduleInput,\n type Task,\n type TaskMappingBase,\n TaskStatus,\n} from '@neofinancial/chrono';\nimport {\n type ClientSession,\n type Collection,\n type Db,\n ObjectId,\n type OptionalId,\n type UpdateFilter,\n type WithId,\n} from 'mongodb';\nimport { ensureIndexes, IndexNames } from './mongo-indexes';\n\nconst DEFAULT_COLLECTION_NAME = 'chrono-tasks';\n\nexport type ChronoMongoDatastoreConfig = {\n /**\n * The TTL (in seconds) for completed documents.\n *\n * @default 60 * 60 * 24 * 30 // 30 days\n * @type {number}\n */\n completedDocumentTTLSeconds?: number;\n\n /**\n * The name of the collection to use for the datastore.\n *\n * @type {string}\n */\n collectionName: string;\n};\n\nexport type MongoDatastoreOptions = {\n session?: ClientSession;\n};\n\nexport type TaskDocument<TaskKind, TaskData> = WithId<Omit<Task<TaskKind, TaskData>, 'id'>>;\n\nexport class ChronoMongoDatastore<TaskMapping extends TaskMappingBase>\n implements Datastore<TaskMapping, MongoDatastoreOptions>\n{\n private config: ChronoMongoDatastoreConfig;\n private database: Db | undefined;\n private databaseResolvers: Array<(database: Db) => void> = [];\n\n constructor(config?: Partial<ChronoMongoDatastoreConfig>) {\n this.config = {\n completedDocumentTTLSeconds: config?.completedDocumentTTLSeconds,\n collectionName: config?.collectionName || DEFAULT_COLLECTION_NAME,\n };\n }\n\n /**\n * Sets the database connection for the datastore. Ensures that the indexes are created and resolves any pending promises waiting for the database.\n *\n * @param database - The database to set.\n */\n async initialize(database: Db): Promise<void> {\n if (this.database) {\n throw new Error('Database connection already set');\n }\n\n await ensureIndexes(database.collection(this.config.collectionName), {\n expireAfterSeconds: this.config.completedDocumentTTLSeconds,\n });\n\n this.database = database;\n\n const resolvers = this.databaseResolvers.splice(0);\n for (const resolve of resolvers) {\n resolve(database);\n }\n }\n\n /**\n * Asyncronously gets the database connection for the datastore. If the database is not set, it will return a promise that resolves when the database is set.\n *\n * @returns The database connection.\n */\n public async getDatabase(): Promise<Db> {\n if (this.database) {\n return this.database;\n }\n\n return new Promise<Db>((resolve) => {\n this.databaseResolvers.push(resolve);\n });\n }\n\n async schedule<TaskKind extends keyof TaskMapping>(\n input: ScheduleInput<TaskKind, TaskMapping[TaskKind], MongoDatastoreOptions>,\n ): Promise<Task<TaskKind, TaskMapping[TaskKind]>> {\n const createInput: OptionalId<TaskDocument<TaskKind, TaskMapping[TaskKind]>> = {\n kind: input.kind,\n status: TaskStatus.PENDING,\n data: input.data,\n priority: input.priority,\n idempotencyKey: input.idempotencyKey,\n originalScheduleDate: input.when,\n scheduledAt: input.when,\n retryCount: 0,\n };\n\n try {\n const database = await this.getDatabase();\n const results = await database.collection(this.config.collectionName).insertOne(createInput, {\n ...(input?.datastoreOptions?.session ? { session: input.datastoreOptions.session } : undefined),\n ignoreUndefined: true,\n });\n\n if (results.acknowledged) {\n return this.toObject({ _id: results.insertedId, ...createInput });\n }\n } catch (error) {\n if (\n input.idempotencyKey &&\n error instanceof Error &&\n 'code' in error &&\n (error.code === 11000 || error.code === 11001)\n ) {\n const collection = await this.collection<TaskKind>();\n const existingTask = await collection.findOne(\n {\n idempotencyKey: input.idempotencyKey,\n },\n {\n hint: IndexNames.IDEMPOTENCY_KEY_INDEX,\n ...(input.datastoreOptions?.session ? { session: input.datastoreOptions.session } : undefined),\n },\n );\n\n if (existingTask) {\n return this.toObject(existingTask);\n }\n\n throw new Error(\n `Failed to find existing task with idempotency key ${input.idempotencyKey} despite unique index error`,\n );\n }\n throw error;\n }\n\n throw new Error(`Failed to insert ${String(input.kind)} document`);\n }\n\n async delete<TaskKind extends Extract<keyof TaskMapping, string>>(\n key: DeleteInput<TaskKind>,\n options?: DeleteOptions,\n ): Promise<Task<TaskKind, TaskMapping[TaskKind]> | undefined> {\n const filter =\n typeof key === 'string' ? { _id: new ObjectId(key) } : { kind: key.kind, idempotencyKey: key.idempotencyKey };\n const collection = await this.collection<TaskKind>();\n const task = await collection.findOneAndDelete({\n ...filter,\n ...(options?.force ? {} : { status: TaskStatus.PENDING }),\n });\n\n if (!task) {\n if (options?.force) {\n return;\n }\n\n const description =\n typeof key === 'string'\n ? `with id ${key}`\n : `with kind ${String(key.kind)} and idempotencyKey ${key.idempotencyKey}`;\n\n throw new Error(`Task ${description} can not be deleted as it may not exist or it's not in PENDING status.`);\n }\n\n return this.toObject(task);\n }\n\n async claim<TaskKind extends Extract<keyof TaskMapping, string>>(\n input: ClaimTaskInput<TaskKind>,\n ): Promise<Task<TaskKind, TaskMapping[TaskKind]> | undefined> {\n const now = new Date();\n const collection = await this.collection<TaskKind>();\n const task = await collection.findOneAndUpdate(\n {\n kind: input.kind,\n scheduledAt: { $lte: now },\n $or: [\n { status: TaskStatus.PENDING },\n {\n status: TaskStatus.CLAIMED,\n claimedAt: {\n $lte: new Date(now.getTime() - input.claimStaleTimeoutMs),\n },\n },\n ],\n },\n { $set: { status: TaskStatus.CLAIMED, claimedAt: now } },\n {\n sort: { priority: -1, scheduledAt: 1 },\n // hint: IndexNames.CLAIM_DOCUMENT_INDEX as unknown as Document,\n returnDocument: 'after',\n },\n );\n\n return task ? this.toObject(task) : undefined;\n }\n\n async retry<TaskKind extends keyof TaskMapping>(\n taskId: string,\n retryAt: Date,\n ): Promise<Task<TaskKind, TaskMapping[TaskKind]>> {\n const taskDocument = await this.updateOrThrow<TaskKind>(taskId, {\n $set: {\n status: TaskStatus.PENDING,\n scheduledAt: retryAt,\n },\n $inc: {\n retryCount: 1,\n },\n });\n\n return this.toObject(taskDocument);\n }\n\n async complete<TaskKind extends keyof TaskMapping>(taskId: string): Promise<Task<TaskKind, TaskMapping[TaskKind]>> {\n const now = new Date();\n\n const task = await this.updateOrThrow<TaskKind>(taskId, {\n $set: {\n status: TaskStatus.COMPLETED,\n completedAt: now,\n lastExecutedAt: now,\n },\n });\n\n return this.toObject(task);\n }\n\n async fail<TaskKind extends keyof TaskMapping>(taskId: string): Promise<Task<TaskKind, TaskMapping[TaskKind]>> {\n const now = new Date();\n\n const task = await this.updateOrThrow<TaskKind>(taskId, {\n $set: {\n status: TaskStatus.FAILED,\n lastExecutedAt: now,\n },\n });\n\n return this.toObject(task);\n }\n\n private async updateOrThrow<TaskKind extends keyof TaskMapping>(\n taskId: string,\n update: UpdateFilter<TaskDocument<TaskKind, TaskMapping[TaskKind]>>,\n ): Promise<TaskDocument<TaskKind, TaskMapping[TaskKind]>> {\n const collection = await this.collection<TaskKind>();\n const document = await collection.findOneAndUpdate({ _id: new ObjectId(taskId) }, update, {\n returnDocument: 'after',\n });\n\n if (!document) {\n throw new Error(`Task with ID ${taskId} not found`);\n }\n return document;\n }\n\n private async collection<TaskKind extends keyof TaskMapping>(): Promise<\n Collection<TaskDocument<TaskKind, TaskMapping[TaskKind]>>\n > {\n const database = await this.getDatabase();\n return database.collection<TaskDocument<TaskKind, TaskMapping[TaskKind]>>(this.config.collectionName);\n }\n\n private toObject<TaskKind extends keyof TaskMapping>(\n document: TaskDocument<TaskKind, TaskMapping[TaskKind]>,\n ): Task<TaskKind, TaskMapping[TaskKind]> {\n return {\n id: document._id.toHexString(),\n data: document.data,\n kind: document.kind,\n status: document.status,\n priority: document.priority ?? undefined,\n idempotencyKey: document.idempotencyKey ?? undefined,\n originalScheduleDate: document.originalScheduleDate,\n scheduledAt: document.scheduledAt,\n claimedAt: document.claimedAt ?? undefined,\n completedAt: document.completedAt ?? undefined,\n retryCount: document.retryCount,\n };\n }\n}\n"],"mappings":"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"}
|
|
@@ -4,17 +4,17 @@ import { ClientSession, Db } from "mongodb";
|
|
|
4
4
|
//#region src/chrono-mongo-datastore.d.ts
|
|
5
5
|
type ChronoMongoDatastoreConfig = {
|
|
6
6
|
/**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
* The TTL (in seconds) for completed documents.
|
|
8
|
+
*
|
|
9
|
+
* @default 60 * 60 * 24 * 30 // 30 days
|
|
10
|
+
* @type {number}
|
|
11
|
+
*/
|
|
12
12
|
completedDocumentTTLSeconds?: number;
|
|
13
13
|
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
* The name of the collection to use for the datastore.
|
|
15
|
+
*
|
|
16
|
+
* @type {string}
|
|
17
|
+
*/
|
|
18
18
|
collectionName: string;
|
|
19
19
|
};
|
|
20
20
|
type MongoDatastoreOptions = {
|
|
@@ -26,16 +26,16 @@ declare class ChronoMongoDatastore<TaskMapping extends TaskMappingBase> implemen
|
|
|
26
26
|
private databaseResolvers;
|
|
27
27
|
constructor(config?: Partial<ChronoMongoDatastoreConfig>);
|
|
28
28
|
/**
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
* Sets the database connection for the datastore. Ensures that the indexes are created and resolves any pending promises waiting for the database.
|
|
30
|
+
*
|
|
31
|
+
* @param database - The database to set.
|
|
32
|
+
*/
|
|
33
33
|
initialize(database: Db): Promise<void>;
|
|
34
34
|
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
* Asyncronously gets the database connection for the datastore. If the database is not set, it will return a promise that resolves when the database is set.
|
|
36
|
+
*
|
|
37
|
+
* @returns The database connection.
|
|
38
|
+
*/
|
|
39
39
|
getDatabase(): Promise<Db>;
|
|
40
40
|
schedule<TaskKind extends keyof TaskMapping>(input: ScheduleInput<TaskKind, TaskMapping[TaskKind], MongoDatastoreOptions>): Promise<Task<TaskKind, TaskMapping[TaskKind]>>;
|
|
41
41
|
delete<TaskKind extends Extract<keyof TaskMapping, string>>(key: DeleteInput<TaskKind>, options?: DeleteOptions): Promise<Task<TaskKind, TaskMapping[TaskKind]> | undefined>;
|
|
@@ -49,4 +49,4 @@ declare class ChronoMongoDatastore<TaskMapping extends TaskMappingBase> implemen
|
|
|
49
49
|
}
|
|
50
50
|
//#endregion
|
|
51
51
|
export { ChronoMongoDatastore, type ChronoMongoDatastoreConfig, type MongoDatastoreOptions };
|
|
52
|
-
//# sourceMappingURL=index.d.
|
|
52
|
+
//# sourceMappingURL=index.d.cts.map
|
package/build/index.d.mts
CHANGED
|
@@ -4,17 +4,17 @@ import { ClientSession, Db } from "mongodb";
|
|
|
4
4
|
//#region src/chrono-mongo-datastore.d.ts
|
|
5
5
|
type ChronoMongoDatastoreConfig = {
|
|
6
6
|
/**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
* The TTL (in seconds) for completed documents.
|
|
8
|
+
*
|
|
9
|
+
* @default 60 * 60 * 24 * 30 // 30 days
|
|
10
|
+
* @type {number}
|
|
11
|
+
*/
|
|
12
12
|
completedDocumentTTLSeconds?: number;
|
|
13
13
|
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
* The name of the collection to use for the datastore.
|
|
15
|
+
*
|
|
16
|
+
* @type {string}
|
|
17
|
+
*/
|
|
18
18
|
collectionName: string;
|
|
19
19
|
};
|
|
20
20
|
type MongoDatastoreOptions = {
|
|
@@ -26,16 +26,16 @@ declare class ChronoMongoDatastore<TaskMapping extends TaskMappingBase> implemen
|
|
|
26
26
|
private databaseResolvers;
|
|
27
27
|
constructor(config?: Partial<ChronoMongoDatastoreConfig>);
|
|
28
28
|
/**
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
* Sets the database connection for the datastore. Ensures that the indexes are created and resolves any pending promises waiting for the database.
|
|
30
|
+
*
|
|
31
|
+
* @param database - The database to set.
|
|
32
|
+
*/
|
|
33
33
|
initialize(database: Db): Promise<void>;
|
|
34
34
|
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
* Asyncronously gets the database connection for the datastore. If the database is not set, it will return a promise that resolves when the database is set.
|
|
36
|
+
*
|
|
37
|
+
* @returns The database connection.
|
|
38
|
+
*/
|
|
39
39
|
getDatabase(): Promise<Db>;
|
|
40
40
|
schedule<TaskKind extends keyof TaskMapping>(input: ScheduleInput<TaskKind, TaskMapping[TaskKind], MongoDatastoreOptions>): Promise<Task<TaskKind, TaskMapping[TaskKind]>>;
|
|
41
41
|
delete<TaskKind extends Extract<keyof TaskMapping, string>>(key: DeleteInput<TaskKind>, options?: DeleteOptions): Promise<Task<TaskKind, TaskMapping[TaskKind]> | undefined>;
|
package/build/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["createInput: OptionalId<TaskDocument<TaskKind, TaskMapping[TaskKind]>>"],"sources":["../src/mongo-indexes.ts","../src/chrono-mongo-datastore.ts"],"sourcesContent":["import { TaskStatus } from '@neofinancial/chrono';\nimport type { Collection } from 'mongodb';\n\nexport const DEFAULT_EXPIRY_SECONDS = 60 * 60 * 24 * 30; // 30 days\n\nexport const IndexNames = {\n COMPLETED_DOCUMENT_TTL_INDEX: 'chrono-completed-document-ttl-index',\n CLAIM_DOCUMENT_INDEX: 'chrono-claim-document-index',\n IDEMPOTENCY_KEY_INDEX: 'chrono-idempotency-key-index',\n};\n\nexport type IndexDefinitionOptions = {\n expireAfterSeconds?: number;\n};\n\nexport async function ensureIndexes(collection: Collection, options: IndexDefinitionOptions): Promise<void> {\n await collection.createIndex(\n { completedAt: -1 },\n {\n partialFilterExpression: {\n completedAt: { $exists: true },\n status: { $eq: TaskStatus.COMPLETED },\n },\n expireAfterSeconds: options.expireAfterSeconds || DEFAULT_EXPIRY_SECONDS,\n name: IndexNames.COMPLETED_DOCUMENT_TTL_INDEX,\n },\n );\n\n await collection.createIndex(\n { kind: 1, status: 1, scheduledAt: 1, priority: -1, claimedAt: 1 },\n { name: IndexNames.CLAIM_DOCUMENT_INDEX },\n );\n\n await collection.createIndex(\n { idempotencyKey: 1 },\n { name: IndexNames.IDEMPOTENCY_KEY_INDEX, unique: true, sparse: true },\n );\n}\n","import {\n type ClaimTaskInput,\n type Datastore,\n type DeleteInput,\n type DeleteOptions,\n type ScheduleInput,\n type Task,\n type TaskMappingBase,\n TaskStatus,\n} from '@neofinancial/chrono';\nimport {\n type ClientSession,\n type Collection,\n type Db,\n ObjectId,\n type OptionalId,\n type UpdateFilter,\n type WithId,\n} from 'mongodb';\nimport { ensureIndexes, IndexNames } from './mongo-indexes';\n\nconst DEFAULT_COLLECTION_NAME = 'chrono-tasks';\n\nexport type ChronoMongoDatastoreConfig = {\n /**\n * The TTL (in seconds) for completed documents.\n *\n * @default 60 * 60 * 24 * 30 // 30 days\n * @type {number}\n */\n completedDocumentTTLSeconds?: number;\n\n /**\n * The name of the collection to use for the datastore.\n *\n * @type {string}\n */\n collectionName: string;\n};\n\nexport type MongoDatastoreOptions = {\n session?: ClientSession;\n};\n\nexport type TaskDocument<TaskKind, TaskData> = WithId<Omit<Task<TaskKind, TaskData>, 'id'>>;\n\nexport class ChronoMongoDatastore<TaskMapping extends TaskMappingBase>\n implements Datastore<TaskMapping, MongoDatastoreOptions>\n{\n private config: ChronoMongoDatastoreConfig;\n private database: Db | undefined;\n private databaseResolvers: Array<(database: Db) => void> = [];\n\n constructor(config?: Partial<ChronoMongoDatastoreConfig>) {\n this.config = {\n completedDocumentTTLSeconds: config?.completedDocumentTTLSeconds,\n collectionName: config?.collectionName || DEFAULT_COLLECTION_NAME,\n };\n }\n\n /**\n * Sets the database connection for the datastore. Ensures that the indexes are created and resolves any pending promises waiting for the database.\n *\n * @param database - The database to set.\n */\n async initialize(database: Db) {\n if (this.database) {\n throw new Error('Database connection already set');\n }\n\n await ensureIndexes(database.collection(this.config.collectionName), {\n expireAfterSeconds: this.config.completedDocumentTTLSeconds,\n });\n\n this.database = database;\n\n const resolvers = this.databaseResolvers.splice(0);\n for (const resolve of resolvers) {\n resolve(database);\n }\n }\n\n /**\n * Asyncronously gets the database connection for the datastore. If the database is not set, it will return a promise that resolves when the database is set.\n *\n * @returns The database connection.\n */\n public async getDatabase(): Promise<Db> {\n if (this.database) {\n return this.database;\n }\n\n return new Promise<Db>((resolve) => {\n this.databaseResolvers.push(resolve);\n });\n }\n\n async schedule<TaskKind extends keyof TaskMapping>(\n input: ScheduleInput<TaskKind, TaskMapping[TaskKind], MongoDatastoreOptions>,\n ): Promise<Task<TaskKind, TaskMapping[TaskKind]>> {\n const createInput: OptionalId<TaskDocument<TaskKind, TaskMapping[TaskKind]>> = {\n kind: input.kind,\n status: TaskStatus.PENDING,\n data: input.data,\n priority: input.priority,\n idempotencyKey: input.idempotencyKey,\n originalScheduleDate: input.when,\n scheduledAt: input.when,\n retryCount: 0,\n };\n\n try {\n const database = await this.getDatabase();\n const results = await database.collection(this.config.collectionName).insertOne(createInput, {\n ...(input?.datastoreOptions?.session ? { session: input.datastoreOptions.session } : undefined),\n ignoreUndefined: true,\n });\n\n if (results.acknowledged) {\n return this.toObject({ _id: results.insertedId, ...createInput });\n }\n } catch (error) {\n if (\n input.idempotencyKey &&\n error instanceof Error &&\n 'code' in error &&\n (error.code === 11000 || error.code === 11001)\n ) {\n const collection = await this.collection<TaskKind>();\n const existingTask = await collection.findOne(\n {\n idempotencyKey: input.idempotencyKey,\n },\n {\n hint: IndexNames.IDEMPOTENCY_KEY_INDEX,\n ...(input.datastoreOptions?.session ? { session: input.datastoreOptions.session } : undefined),\n },\n );\n\n if (existingTask) {\n return this.toObject(existingTask);\n }\n\n throw new Error(\n `Failed to find existing task with idempotency key ${input.idempotencyKey} despite unique index error`,\n );\n }\n throw error;\n }\n\n throw new Error(`Failed to insert ${String(input.kind)} document`);\n }\n\n async delete<TaskKind extends Extract<keyof TaskMapping, string>>(\n key: DeleteInput<TaskKind>,\n options?: DeleteOptions,\n ): Promise<Task<TaskKind, TaskMapping[TaskKind]> | undefined> {\n const filter =\n typeof key === 'string' ? { _id: new ObjectId(key) } : { kind: key.kind, idempotencyKey: key.idempotencyKey };\n const collection = await this.collection<TaskKind>();\n const task = await collection.findOneAndDelete({\n ...filter,\n ...(options?.force ? {} : { status: TaskStatus.PENDING }),\n });\n\n if (!task) {\n if (options?.force) {\n return;\n }\n\n const description =\n typeof key === 'string'\n ? `with id ${key}`\n : `with kind ${String(key.kind)} and idempotencyKey ${key.idempotencyKey}`;\n\n throw new Error(`Task ${description} can not be deleted as it may not exist or it's not in PENDING status.`);\n }\n\n return this.toObject(task);\n }\n\n async claim<TaskKind extends Extract<keyof TaskMapping, string>>(\n input: ClaimTaskInput<TaskKind>,\n ): Promise<Task<TaskKind, TaskMapping[TaskKind]> | undefined> {\n const now = new Date();\n const collection = await this.collection<TaskKind>();\n const task = await collection.findOneAndUpdate(\n {\n kind: input.kind,\n scheduledAt: { $lte: now },\n $or: [\n { status: TaskStatus.PENDING },\n {\n status: TaskStatus.CLAIMED,\n claimedAt: {\n $lte: new Date(now.getTime() - input.claimStaleTimeoutMs),\n },\n },\n ],\n },\n { $set: { status: TaskStatus.CLAIMED, claimedAt: now } },\n {\n sort: { priority: -1, scheduledAt: 1 },\n // hint: IndexNames.CLAIM_DOCUMENT_INDEX as unknown as Document,\n returnDocument: 'after',\n },\n );\n\n return task ? this.toObject(task) : undefined;\n }\n\n async retry<TaskKind extends keyof TaskMapping>(\n taskId: string,\n retryAt: Date,\n ): Promise<Task<TaskKind, TaskMapping[TaskKind]>> {\n const taskDocument = await this.updateOrThrow<TaskKind>(taskId, {\n $set: {\n status: TaskStatus.PENDING,\n scheduledAt: retryAt,\n },\n $inc: {\n retryCount: 1,\n },\n });\n\n return this.toObject(taskDocument);\n }\n\n async complete<TaskKind extends keyof TaskMapping>(taskId: string): Promise<Task<TaskKind, TaskMapping[TaskKind]>> {\n const now = new Date();\n\n const task = await this.updateOrThrow<TaskKind>(taskId, {\n $set: {\n status: TaskStatus.COMPLETED,\n completedAt: now,\n lastExecutedAt: now,\n },\n });\n\n return this.toObject(task);\n }\n\n async fail<TaskKind extends keyof TaskMapping>(taskId: string): Promise<Task<TaskKind, TaskMapping[TaskKind]>> {\n const now = new Date();\n\n const task = await this.updateOrThrow<TaskKind>(taskId, {\n $set: {\n status: TaskStatus.FAILED,\n lastExecutedAt: now,\n },\n });\n\n return this.toObject(task);\n }\n\n private async updateOrThrow<TaskKind extends keyof TaskMapping>(\n taskId: string,\n update: UpdateFilter<TaskDocument<TaskKind, TaskMapping[TaskKind]>>,\n ): Promise<TaskDocument<TaskKind, TaskMapping[TaskKind]>> {\n const collection = await this.collection<TaskKind>();\n const document = await collection.findOneAndUpdate({ _id: new ObjectId(taskId) }, update, {\n returnDocument: 'after',\n });\n\n if (!document) {\n throw new Error(`Task with ID ${taskId} not found`);\n }\n return document;\n }\n\n private async collection<TaskKind extends keyof TaskMapping>(): Promise<\n Collection<TaskDocument<TaskKind, TaskMapping[TaskKind]>>\n > {\n const database = await this.getDatabase();\n return database.collection<TaskDocument<TaskKind, TaskMapping[TaskKind]>>(this.config.collectionName);\n }\n\n private toObject<TaskKind extends keyof TaskMapping>(\n document: TaskDocument<TaskKind, TaskMapping[TaskKind]>,\n ): Task<TaskKind, TaskMapping[TaskKind]> {\n return {\n id: document._id.toHexString(),\n data: document.data,\n kind: document.kind,\n status: document.status,\n priority: document.priority ?? undefined,\n idempotencyKey: document.idempotencyKey ?? undefined,\n originalScheduleDate: document.originalScheduleDate,\n scheduledAt: document.scheduledAt,\n claimedAt: document.claimedAt ?? undefined,\n completedAt: document.completedAt ?? undefined,\n retryCount: document.retryCount,\n };\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/mongo-indexes.ts","../src/chrono-mongo-datastore.ts"],"sourcesContent":["import { TaskStatus } from '@neofinancial/chrono';\nimport type { Collection } from 'mongodb';\n\nexport const DEFAULT_EXPIRY_SECONDS: number = 60 * 60 * 24 * 30; // 30 days\n\nexport const IndexNames = {\n COMPLETED_DOCUMENT_TTL_INDEX: 'chrono-completed-document-ttl-index',\n CLAIM_DOCUMENT_INDEX: 'chrono-claim-document-index',\n IDEMPOTENCY_KEY_INDEX: 'chrono-idempotency-key-index',\n};\n\nexport type IndexDefinitionOptions = {\n expireAfterSeconds?: number;\n};\n\nexport async function ensureIndexes(collection: Collection, options: IndexDefinitionOptions): Promise<void> {\n await collection.createIndex(\n { completedAt: -1 },\n {\n partialFilterExpression: {\n completedAt: { $exists: true },\n status: { $eq: TaskStatus.COMPLETED },\n },\n expireAfterSeconds: options.expireAfterSeconds || DEFAULT_EXPIRY_SECONDS,\n name: IndexNames.COMPLETED_DOCUMENT_TTL_INDEX,\n },\n );\n\n await collection.createIndex(\n { kind: 1, status: 1, scheduledAt: 1, priority: -1, claimedAt: 1 },\n { name: IndexNames.CLAIM_DOCUMENT_INDEX },\n );\n\n await collection.createIndex(\n { idempotencyKey: 1 },\n { name: IndexNames.IDEMPOTENCY_KEY_INDEX, unique: true, sparse: true },\n );\n}\n","import {\n type ClaimTaskInput,\n type Datastore,\n type DeleteInput,\n type DeleteOptions,\n type ScheduleInput,\n type Task,\n type TaskMappingBase,\n TaskStatus,\n} from '@neofinancial/chrono';\nimport {\n type ClientSession,\n type Collection,\n type Db,\n ObjectId,\n type OptionalId,\n type UpdateFilter,\n type WithId,\n} from 'mongodb';\nimport { ensureIndexes, IndexNames } from './mongo-indexes';\n\nconst DEFAULT_COLLECTION_NAME = 'chrono-tasks';\n\nexport type ChronoMongoDatastoreConfig = {\n /**\n * The TTL (in seconds) for completed documents.\n *\n * @default 60 * 60 * 24 * 30 // 30 days\n * @type {number}\n */\n completedDocumentTTLSeconds?: number;\n\n /**\n * The name of the collection to use for the datastore.\n *\n * @type {string}\n */\n collectionName: string;\n};\n\nexport type MongoDatastoreOptions = {\n session?: ClientSession;\n};\n\nexport type TaskDocument<TaskKind, TaskData> = WithId<Omit<Task<TaskKind, TaskData>, 'id'>>;\n\nexport class ChronoMongoDatastore<TaskMapping extends TaskMappingBase>\n implements Datastore<TaskMapping, MongoDatastoreOptions>\n{\n private config: ChronoMongoDatastoreConfig;\n private database: Db | undefined;\n private databaseResolvers: Array<(database: Db) => void> = [];\n\n constructor(config?: Partial<ChronoMongoDatastoreConfig>) {\n this.config = {\n completedDocumentTTLSeconds: config?.completedDocumentTTLSeconds,\n collectionName: config?.collectionName || DEFAULT_COLLECTION_NAME,\n };\n }\n\n /**\n * Sets the database connection for the datastore. Ensures that the indexes are created and resolves any pending promises waiting for the database.\n *\n * @param database - The database to set.\n */\n async initialize(database: Db): Promise<void> {\n if (this.database) {\n throw new Error('Database connection already set');\n }\n\n await ensureIndexes(database.collection(this.config.collectionName), {\n expireAfterSeconds: this.config.completedDocumentTTLSeconds,\n });\n\n this.database = database;\n\n const resolvers = this.databaseResolvers.splice(0);\n for (const resolve of resolvers) {\n resolve(database);\n }\n }\n\n /**\n * Asyncronously gets the database connection for the datastore. If the database is not set, it will return a promise that resolves when the database is set.\n *\n * @returns The database connection.\n */\n public async getDatabase(): Promise<Db> {\n if (this.database) {\n return this.database;\n }\n\n return new Promise<Db>((resolve) => {\n this.databaseResolvers.push(resolve);\n });\n }\n\n async schedule<TaskKind extends keyof TaskMapping>(\n input: ScheduleInput<TaskKind, TaskMapping[TaskKind], MongoDatastoreOptions>,\n ): Promise<Task<TaskKind, TaskMapping[TaskKind]>> {\n const createInput: OptionalId<TaskDocument<TaskKind, TaskMapping[TaskKind]>> = {\n kind: input.kind,\n status: TaskStatus.PENDING,\n data: input.data,\n priority: input.priority,\n idempotencyKey: input.idempotencyKey,\n originalScheduleDate: input.when,\n scheduledAt: input.when,\n retryCount: 0,\n };\n\n try {\n const database = await this.getDatabase();\n const results = await database.collection(this.config.collectionName).insertOne(createInput, {\n ...(input?.datastoreOptions?.session ? { session: input.datastoreOptions.session } : undefined),\n ignoreUndefined: true,\n });\n\n if (results.acknowledged) {\n return this.toObject({ _id: results.insertedId, ...createInput });\n }\n } catch (error) {\n if (\n input.idempotencyKey &&\n error instanceof Error &&\n 'code' in error &&\n (error.code === 11000 || error.code === 11001)\n ) {\n const collection = await this.collection<TaskKind>();\n const existingTask = await collection.findOne(\n {\n idempotencyKey: input.idempotencyKey,\n },\n {\n hint: IndexNames.IDEMPOTENCY_KEY_INDEX,\n ...(input.datastoreOptions?.session ? { session: input.datastoreOptions.session } : undefined),\n },\n );\n\n if (existingTask) {\n return this.toObject(existingTask);\n }\n\n throw new Error(\n `Failed to find existing task with idempotency key ${input.idempotencyKey} despite unique index error`,\n );\n }\n throw error;\n }\n\n throw new Error(`Failed to insert ${String(input.kind)} document`);\n }\n\n async delete<TaskKind extends Extract<keyof TaskMapping, string>>(\n key: DeleteInput<TaskKind>,\n options?: DeleteOptions,\n ): Promise<Task<TaskKind, TaskMapping[TaskKind]> | undefined> {\n const filter =\n typeof key === 'string' ? { _id: new ObjectId(key) } : { kind: key.kind, idempotencyKey: key.idempotencyKey };\n const collection = await this.collection<TaskKind>();\n const task = await collection.findOneAndDelete({\n ...filter,\n ...(options?.force ? {} : { status: TaskStatus.PENDING }),\n });\n\n if (!task) {\n if (options?.force) {\n return;\n }\n\n const description =\n typeof key === 'string'\n ? `with id ${key}`\n : `with kind ${String(key.kind)} and idempotencyKey ${key.idempotencyKey}`;\n\n throw new Error(`Task ${description} can not be deleted as it may not exist or it's not in PENDING status.`);\n }\n\n return this.toObject(task);\n }\n\n async claim<TaskKind extends Extract<keyof TaskMapping, string>>(\n input: ClaimTaskInput<TaskKind>,\n ): Promise<Task<TaskKind, TaskMapping[TaskKind]> | undefined> {\n const now = new Date();\n const collection = await this.collection<TaskKind>();\n const task = await collection.findOneAndUpdate(\n {\n kind: input.kind,\n scheduledAt: { $lte: now },\n $or: [\n { status: TaskStatus.PENDING },\n {\n status: TaskStatus.CLAIMED,\n claimedAt: {\n $lte: new Date(now.getTime() - input.claimStaleTimeoutMs),\n },\n },\n ],\n },\n { $set: { status: TaskStatus.CLAIMED, claimedAt: now } },\n {\n sort: { priority: -1, scheduledAt: 1 },\n // hint: IndexNames.CLAIM_DOCUMENT_INDEX as unknown as Document,\n returnDocument: 'after',\n },\n );\n\n return task ? this.toObject(task) : undefined;\n }\n\n async retry<TaskKind extends keyof TaskMapping>(\n taskId: string,\n retryAt: Date,\n ): Promise<Task<TaskKind, TaskMapping[TaskKind]>> {\n const taskDocument = await this.updateOrThrow<TaskKind>(taskId, {\n $set: {\n status: TaskStatus.PENDING,\n scheduledAt: retryAt,\n },\n $inc: {\n retryCount: 1,\n },\n });\n\n return this.toObject(taskDocument);\n }\n\n async complete<TaskKind extends keyof TaskMapping>(taskId: string): Promise<Task<TaskKind, TaskMapping[TaskKind]>> {\n const now = new Date();\n\n const task = await this.updateOrThrow<TaskKind>(taskId, {\n $set: {\n status: TaskStatus.COMPLETED,\n completedAt: now,\n lastExecutedAt: now,\n },\n });\n\n return this.toObject(task);\n }\n\n async fail<TaskKind extends keyof TaskMapping>(taskId: string): Promise<Task<TaskKind, TaskMapping[TaskKind]>> {\n const now = new Date();\n\n const task = await this.updateOrThrow<TaskKind>(taskId, {\n $set: {\n status: TaskStatus.FAILED,\n lastExecutedAt: now,\n },\n });\n\n return this.toObject(task);\n }\n\n private async updateOrThrow<TaskKind extends keyof TaskMapping>(\n taskId: string,\n update: UpdateFilter<TaskDocument<TaskKind, TaskMapping[TaskKind]>>,\n ): Promise<TaskDocument<TaskKind, TaskMapping[TaskKind]>> {\n const collection = await this.collection<TaskKind>();\n const document = await collection.findOneAndUpdate({ _id: new ObjectId(taskId) }, update, {\n returnDocument: 'after',\n });\n\n if (!document) {\n throw new Error(`Task with ID ${taskId} not found`);\n }\n return document;\n }\n\n private async collection<TaskKind extends keyof TaskMapping>(): Promise<\n Collection<TaskDocument<TaskKind, TaskMapping[TaskKind]>>\n > {\n const database = await this.getDatabase();\n return database.collection<TaskDocument<TaskKind, TaskMapping[TaskKind]>>(this.config.collectionName);\n }\n\n private toObject<TaskKind extends keyof TaskMapping>(\n document: TaskDocument<TaskKind, TaskMapping[TaskKind]>,\n ): Task<TaskKind, TaskMapping[TaskKind]> {\n return {\n id: document._id.toHexString(),\n data: document.data,\n kind: document.kind,\n status: document.status,\n priority: document.priority ?? undefined,\n idempotencyKey: document.idempotencyKey ?? undefined,\n originalScheduleDate: document.originalScheduleDate,\n scheduledAt: document.scheduledAt,\n claimedAt: document.claimedAt ?? undefined,\n completedAt: document.completedAt ?? undefined,\n retryCount: document.retryCount,\n };\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neofinancial/chrono-mongo-datastore",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "MongoDB datastore implementation for Chrono task scheduling system",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -11,6 +11,13 @@
|
|
|
11
11
|
"type": "git",
|
|
12
12
|
"url": "https://github.com/neofinancial/chrono.git"
|
|
13
13
|
},
|
|
14
|
+
"keywords": [],
|
|
15
|
+
"author": "Neo Financial Engineering <engineering@neofinancial.com>",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"files": [
|
|
18
|
+
"build/**",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
14
21
|
"main": "./build/index.js",
|
|
15
22
|
"module": "./build/index.mjs",
|
|
16
23
|
"types": "./build/index.d.ts",
|
|
@@ -26,26 +33,20 @@
|
|
|
26
33
|
}
|
|
27
34
|
}
|
|
28
35
|
},
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"files": [
|
|
33
|
-
"build/**",
|
|
34
|
-
"README.md"
|
|
35
|
-
],
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"mongodb": "^6"
|
|
38
|
+
},
|
|
36
39
|
"devDependencies": {
|
|
37
|
-
"mongodb": "^6",
|
|
38
40
|
"mongodb-memory-server": "^10.1.4",
|
|
39
|
-
"@neofinancial/chrono": "0.5.
|
|
41
|
+
"@neofinancial/chrono": "0.5.2"
|
|
40
42
|
},
|
|
41
43
|
"peerDependencies": {
|
|
42
|
-
"
|
|
43
|
-
"@neofinancial/chrono": "0.5.1"
|
|
44
|
+
"@neofinancial/chrono": "0.5.2"
|
|
44
45
|
},
|
|
45
46
|
"scripts": {
|
|
46
|
-
"clean": "rimraf ./build",
|
|
47
|
+
"clean": "rimraf ./build ./node_modules",
|
|
47
48
|
"build": "tsdown",
|
|
48
|
-
"typecheck": "tsc -p ./tsconfig.json --noEmit",
|
|
49
|
+
"typecheck": "tsc -p ./tsconfig.json --noEmit && tsc -p ./test/tsconfig.json --noEmit",
|
|
49
50
|
"test": "NODE_ENV=test TZ=UTC vitest run"
|
|
50
51
|
}
|
|
51
52
|
}
|
package/build/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["TaskStatus","createInput: OptionalId<TaskDocument<TaskKind, TaskMapping[TaskKind]>>","TaskStatus","ObjectId"],"sources":["../src/mongo-indexes.ts","../src/chrono-mongo-datastore.ts"],"sourcesContent":["import { TaskStatus } from '@neofinancial/chrono';\nimport type { Collection } from 'mongodb';\n\nexport const DEFAULT_EXPIRY_SECONDS = 60 * 60 * 24 * 30; // 30 days\n\nexport const IndexNames = {\n COMPLETED_DOCUMENT_TTL_INDEX: 'chrono-completed-document-ttl-index',\n CLAIM_DOCUMENT_INDEX: 'chrono-claim-document-index',\n IDEMPOTENCY_KEY_INDEX: 'chrono-idempotency-key-index',\n};\n\nexport type IndexDefinitionOptions = {\n expireAfterSeconds?: number;\n};\n\nexport async function ensureIndexes(collection: Collection, options: IndexDefinitionOptions): Promise<void> {\n await collection.createIndex(\n { completedAt: -1 },\n {\n partialFilterExpression: {\n completedAt: { $exists: true },\n status: { $eq: TaskStatus.COMPLETED },\n },\n expireAfterSeconds: options.expireAfterSeconds || DEFAULT_EXPIRY_SECONDS,\n name: IndexNames.COMPLETED_DOCUMENT_TTL_INDEX,\n },\n );\n\n await collection.createIndex(\n { kind: 1, status: 1, scheduledAt: 1, priority: -1, claimedAt: 1 },\n { name: IndexNames.CLAIM_DOCUMENT_INDEX },\n );\n\n await collection.createIndex(\n { idempotencyKey: 1 },\n { name: IndexNames.IDEMPOTENCY_KEY_INDEX, unique: true, sparse: true },\n );\n}\n","import {\n type ClaimTaskInput,\n type Datastore,\n type DeleteInput,\n type DeleteOptions,\n type ScheduleInput,\n type Task,\n type TaskMappingBase,\n TaskStatus,\n} from '@neofinancial/chrono';\nimport {\n type ClientSession,\n type Collection,\n type Db,\n ObjectId,\n type OptionalId,\n type UpdateFilter,\n type WithId,\n} from 'mongodb';\nimport { ensureIndexes, IndexNames } from './mongo-indexes';\n\nconst DEFAULT_COLLECTION_NAME = 'chrono-tasks';\n\nexport type ChronoMongoDatastoreConfig = {\n /**\n * The TTL (in seconds) for completed documents.\n *\n * @default 60 * 60 * 24 * 30 // 30 days\n * @type {number}\n */\n completedDocumentTTLSeconds?: number;\n\n /**\n * The name of the collection to use for the datastore.\n *\n * @type {string}\n */\n collectionName: string;\n};\n\nexport type MongoDatastoreOptions = {\n session?: ClientSession;\n};\n\nexport type TaskDocument<TaskKind, TaskData> = WithId<Omit<Task<TaskKind, TaskData>, 'id'>>;\n\nexport class ChronoMongoDatastore<TaskMapping extends TaskMappingBase>\n implements Datastore<TaskMapping, MongoDatastoreOptions>\n{\n private config: ChronoMongoDatastoreConfig;\n private database: Db | undefined;\n private databaseResolvers: Array<(database: Db) => void> = [];\n\n constructor(config?: Partial<ChronoMongoDatastoreConfig>) {\n this.config = {\n completedDocumentTTLSeconds: config?.completedDocumentTTLSeconds,\n collectionName: config?.collectionName || DEFAULT_COLLECTION_NAME,\n };\n }\n\n /**\n * Sets the database connection for the datastore. Ensures that the indexes are created and resolves any pending promises waiting for the database.\n *\n * @param database - The database to set.\n */\n async initialize(database: Db) {\n if (this.database) {\n throw new Error('Database connection already set');\n }\n\n await ensureIndexes(database.collection(this.config.collectionName), {\n expireAfterSeconds: this.config.completedDocumentTTLSeconds,\n });\n\n this.database = database;\n\n const resolvers = this.databaseResolvers.splice(0);\n for (const resolve of resolvers) {\n resolve(database);\n }\n }\n\n /**\n * Asyncronously gets the database connection for the datastore. If the database is not set, it will return a promise that resolves when the database is set.\n *\n * @returns The database connection.\n */\n public async getDatabase(): Promise<Db> {\n if (this.database) {\n return this.database;\n }\n\n return new Promise<Db>((resolve) => {\n this.databaseResolvers.push(resolve);\n });\n }\n\n async schedule<TaskKind extends keyof TaskMapping>(\n input: ScheduleInput<TaskKind, TaskMapping[TaskKind], MongoDatastoreOptions>,\n ): Promise<Task<TaskKind, TaskMapping[TaskKind]>> {\n const createInput: OptionalId<TaskDocument<TaskKind, TaskMapping[TaskKind]>> = {\n kind: input.kind,\n status: TaskStatus.PENDING,\n data: input.data,\n priority: input.priority,\n idempotencyKey: input.idempotencyKey,\n originalScheduleDate: input.when,\n scheduledAt: input.when,\n retryCount: 0,\n };\n\n try {\n const database = await this.getDatabase();\n const results = await database.collection(this.config.collectionName).insertOne(createInput, {\n ...(input?.datastoreOptions?.session ? { session: input.datastoreOptions.session } : undefined),\n ignoreUndefined: true,\n });\n\n if (results.acknowledged) {\n return this.toObject({ _id: results.insertedId, ...createInput });\n }\n } catch (error) {\n if (\n input.idempotencyKey &&\n error instanceof Error &&\n 'code' in error &&\n (error.code === 11000 || error.code === 11001)\n ) {\n const collection = await this.collection<TaskKind>();\n const existingTask = await collection.findOne(\n {\n idempotencyKey: input.idempotencyKey,\n },\n {\n hint: IndexNames.IDEMPOTENCY_KEY_INDEX,\n ...(input.datastoreOptions?.session ? { session: input.datastoreOptions.session } : undefined),\n },\n );\n\n if (existingTask) {\n return this.toObject(existingTask);\n }\n\n throw new Error(\n `Failed to find existing task with idempotency key ${input.idempotencyKey} despite unique index error`,\n );\n }\n throw error;\n }\n\n throw new Error(`Failed to insert ${String(input.kind)} document`);\n }\n\n async delete<TaskKind extends Extract<keyof TaskMapping, string>>(\n key: DeleteInput<TaskKind>,\n options?: DeleteOptions,\n ): Promise<Task<TaskKind, TaskMapping[TaskKind]> | undefined> {\n const filter =\n typeof key === 'string' ? { _id: new ObjectId(key) } : { kind: key.kind, idempotencyKey: key.idempotencyKey };\n const collection = await this.collection<TaskKind>();\n const task = await collection.findOneAndDelete({\n ...filter,\n ...(options?.force ? {} : { status: TaskStatus.PENDING }),\n });\n\n if (!task) {\n if (options?.force) {\n return;\n }\n\n const description =\n typeof key === 'string'\n ? `with id ${key}`\n : `with kind ${String(key.kind)} and idempotencyKey ${key.idempotencyKey}`;\n\n throw new Error(`Task ${description} can not be deleted as it may not exist or it's not in PENDING status.`);\n }\n\n return this.toObject(task);\n }\n\n async claim<TaskKind extends Extract<keyof TaskMapping, string>>(\n input: ClaimTaskInput<TaskKind>,\n ): Promise<Task<TaskKind, TaskMapping[TaskKind]> | undefined> {\n const now = new Date();\n const collection = await this.collection<TaskKind>();\n const task = await collection.findOneAndUpdate(\n {\n kind: input.kind,\n scheduledAt: { $lte: now },\n $or: [\n { status: TaskStatus.PENDING },\n {\n status: TaskStatus.CLAIMED,\n claimedAt: {\n $lte: new Date(now.getTime() - input.claimStaleTimeoutMs),\n },\n },\n ],\n },\n { $set: { status: TaskStatus.CLAIMED, claimedAt: now } },\n {\n sort: { priority: -1, scheduledAt: 1 },\n // hint: IndexNames.CLAIM_DOCUMENT_INDEX as unknown as Document,\n returnDocument: 'after',\n },\n );\n\n return task ? this.toObject(task) : undefined;\n }\n\n async retry<TaskKind extends keyof TaskMapping>(\n taskId: string,\n retryAt: Date,\n ): Promise<Task<TaskKind, TaskMapping[TaskKind]>> {\n const taskDocument = await this.updateOrThrow<TaskKind>(taskId, {\n $set: {\n status: TaskStatus.PENDING,\n scheduledAt: retryAt,\n },\n $inc: {\n retryCount: 1,\n },\n });\n\n return this.toObject(taskDocument);\n }\n\n async complete<TaskKind extends keyof TaskMapping>(taskId: string): Promise<Task<TaskKind, TaskMapping[TaskKind]>> {\n const now = new Date();\n\n const task = await this.updateOrThrow<TaskKind>(taskId, {\n $set: {\n status: TaskStatus.COMPLETED,\n completedAt: now,\n lastExecutedAt: now,\n },\n });\n\n return this.toObject(task);\n }\n\n async fail<TaskKind extends keyof TaskMapping>(taskId: string): Promise<Task<TaskKind, TaskMapping[TaskKind]>> {\n const now = new Date();\n\n const task = await this.updateOrThrow<TaskKind>(taskId, {\n $set: {\n status: TaskStatus.FAILED,\n lastExecutedAt: now,\n },\n });\n\n return this.toObject(task);\n }\n\n private async updateOrThrow<TaskKind extends keyof TaskMapping>(\n taskId: string,\n update: UpdateFilter<TaskDocument<TaskKind, TaskMapping[TaskKind]>>,\n ): Promise<TaskDocument<TaskKind, TaskMapping[TaskKind]>> {\n const collection = await this.collection<TaskKind>();\n const document = await collection.findOneAndUpdate({ _id: new ObjectId(taskId) }, update, {\n returnDocument: 'after',\n });\n\n if (!document) {\n throw new Error(`Task with ID ${taskId} not found`);\n }\n return document;\n }\n\n private async collection<TaskKind extends keyof TaskMapping>(): Promise<\n Collection<TaskDocument<TaskKind, TaskMapping[TaskKind]>>\n > {\n const database = await this.getDatabase();\n return database.collection<TaskDocument<TaskKind, TaskMapping[TaskKind]>>(this.config.collectionName);\n }\n\n private toObject<TaskKind extends keyof TaskMapping>(\n document: TaskDocument<TaskKind, TaskMapping[TaskKind]>,\n ): Task<TaskKind, TaskMapping[TaskKind]> {\n return {\n id: document._id.toHexString(),\n data: document.data,\n kind: document.kind,\n status: document.status,\n priority: document.priority ?? undefined,\n idempotencyKey: document.idempotencyKey ?? undefined,\n originalScheduleDate: document.originalScheduleDate,\n scheduledAt: document.scheduledAt,\n claimedAt: document.claimedAt ?? undefined,\n completedAt: document.completedAt ?? undefined,\n retryCount: document.retryCount,\n };\n }\n}\n"],"mappings":"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"}
|