@neofinancial/chrono-mongo-datastore 0.4.1-next.0 → 0.5.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/README.md +12 -0
- package/build/index.d.mts +52 -0
- package/build/index.d.ts +52 -1
- package/build/index.js +218 -5
- package/build/index.js.map +1 -1
- package/build/index.mjs +194 -0
- package/build/index.mjs.map +1 -0
- package/package.json +19 -6
- package/build/chrono-mongo-datastore.d.ts +0 -48
- package/build/chrono-mongo-datastore.js +0 -195
- package/build/chrono-mongo-datastore.js.map +0 -1
- package/build/main.d.ts +0 -1
- package/build/main.js +0 -13
- package/build/main.js.map +0 -1
- package/build/mongo-indexes.d.ts +0 -11
- package/build/mongo-indexes.js +0 -24
- package/build/mongo-indexes.js.map +0 -1
- package/build/mongo-task.d.ts +0 -4
- package/build/mongo-task.js +0 -11
- package/build/mongo-task.js.map +0 -1
package/README.md
CHANGED
|
@@ -22,6 +22,18 @@ pnpm add @neofinancial/chrono-mongo-datastore
|
|
|
22
22
|
yarn add @neofinancial/chrono-mongo-datastore
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
This package supports both **CommonJS** and **ES Modules**:
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
// ESM
|
|
29
|
+
import { ChronoMongoDatastore } from "@neofinancial/chrono-mongo-datastore";
|
|
30
|
+
|
|
31
|
+
// CommonJS
|
|
32
|
+
const {
|
|
33
|
+
ChronoMongoDatastore,
|
|
34
|
+
} = require("@neofinancial/chrono-mongo-datastore");
|
|
35
|
+
```
|
|
36
|
+
|
|
25
37
|
## Peer Dependencies
|
|
26
38
|
|
|
27
39
|
`@neofinancial/chrono` and `mongodb`
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ClaimTaskInput, Datastore, DeleteInput, DeleteOptions, ScheduleInput, Task, TaskMappingBase } from "@neofinancial/chrono";
|
|
2
|
+
import { ClientSession, Db } from "mongodb";
|
|
3
|
+
|
|
4
|
+
//#region src/chrono-mongo-datastore.d.ts
|
|
5
|
+
type ChronoMongoDatastoreConfig = {
|
|
6
|
+
/**
|
|
7
|
+
* The TTL (in seconds) for completed documents.
|
|
8
|
+
*
|
|
9
|
+
* @default 60 * 60 * 24 * 30 // 30 days
|
|
10
|
+
* @type {number}
|
|
11
|
+
*/
|
|
12
|
+
completedDocumentTTLSeconds?: number;
|
|
13
|
+
/**
|
|
14
|
+
* The name of the collection to use for the datastore.
|
|
15
|
+
*
|
|
16
|
+
* @type {string}
|
|
17
|
+
*/
|
|
18
|
+
collectionName: string;
|
|
19
|
+
};
|
|
20
|
+
type MongoDatastoreOptions = {
|
|
21
|
+
session?: ClientSession;
|
|
22
|
+
};
|
|
23
|
+
declare class ChronoMongoDatastore<TaskMapping extends TaskMappingBase> implements Datastore<TaskMapping, MongoDatastoreOptions> {
|
|
24
|
+
private config;
|
|
25
|
+
private database;
|
|
26
|
+
private databaseResolvers;
|
|
27
|
+
constructor(config?: Partial<ChronoMongoDatastoreConfig>);
|
|
28
|
+
/**
|
|
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
|
+
initialize(database: Db): Promise<void>;
|
|
34
|
+
/**
|
|
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
|
+
getDatabase(): Promise<Db>;
|
|
40
|
+
schedule<TaskKind extends keyof TaskMapping>(input: ScheduleInput<TaskKind, TaskMapping[TaskKind], MongoDatastoreOptions>): Promise<Task<TaskKind, TaskMapping[TaskKind]>>;
|
|
41
|
+
delete<TaskKind extends Extract<keyof TaskMapping, string>>(key: DeleteInput<TaskKind>, options?: DeleteOptions): Promise<Task<TaskKind, TaskMapping[TaskKind]> | undefined>;
|
|
42
|
+
claim<TaskKind extends Extract<keyof TaskMapping, string>>(input: ClaimTaskInput<TaskKind>): Promise<Task<TaskKind, TaskMapping[TaskKind]> | undefined>;
|
|
43
|
+
retry<TaskKind extends keyof TaskMapping>(taskId: string, retryAt: Date): Promise<Task<TaskKind, TaskMapping[TaskKind]>>;
|
|
44
|
+
complete<TaskKind extends keyof TaskMapping>(taskId: string): Promise<Task<TaskKind, TaskMapping[TaskKind]>>;
|
|
45
|
+
fail<TaskKind extends keyof TaskMapping>(taskId: string): Promise<Task<TaskKind, TaskMapping[TaskKind]>>;
|
|
46
|
+
private updateOrThrow;
|
|
47
|
+
private collection;
|
|
48
|
+
private toObject;
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
export { ChronoMongoDatastore, type ChronoMongoDatastoreConfig, type MongoDatastoreOptions };
|
|
52
|
+
//# sourceMappingURL=index.d.mts.map
|
package/build/index.d.ts
CHANGED
|
@@ -1 +1,52 @@
|
|
|
1
|
-
|
|
1
|
+
import { ClaimTaskInput, Datastore, DeleteInput, DeleteOptions, ScheduleInput, Task, TaskMappingBase } from "@neofinancial/chrono";
|
|
2
|
+
import { ClientSession, Db } from "mongodb";
|
|
3
|
+
|
|
4
|
+
//#region src/chrono-mongo-datastore.d.ts
|
|
5
|
+
type ChronoMongoDatastoreConfig = {
|
|
6
|
+
/**
|
|
7
|
+
* The TTL (in seconds) for completed documents.
|
|
8
|
+
*
|
|
9
|
+
* @default 60 * 60 * 24 * 30 // 30 days
|
|
10
|
+
* @type {number}
|
|
11
|
+
*/
|
|
12
|
+
completedDocumentTTLSeconds?: number;
|
|
13
|
+
/**
|
|
14
|
+
* The name of the collection to use for the datastore.
|
|
15
|
+
*
|
|
16
|
+
* @type {string}
|
|
17
|
+
*/
|
|
18
|
+
collectionName: string;
|
|
19
|
+
};
|
|
20
|
+
type MongoDatastoreOptions = {
|
|
21
|
+
session?: ClientSession;
|
|
22
|
+
};
|
|
23
|
+
declare class ChronoMongoDatastore<TaskMapping extends TaskMappingBase> implements Datastore<TaskMapping, MongoDatastoreOptions> {
|
|
24
|
+
private config;
|
|
25
|
+
private database;
|
|
26
|
+
private databaseResolvers;
|
|
27
|
+
constructor(config?: Partial<ChronoMongoDatastoreConfig>);
|
|
28
|
+
/**
|
|
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
|
+
initialize(database: Db): Promise<void>;
|
|
34
|
+
/**
|
|
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
|
+
getDatabase(): Promise<Db>;
|
|
40
|
+
schedule<TaskKind extends keyof TaskMapping>(input: ScheduleInput<TaskKind, TaskMapping[TaskKind], MongoDatastoreOptions>): Promise<Task<TaskKind, TaskMapping[TaskKind]>>;
|
|
41
|
+
delete<TaskKind extends Extract<keyof TaskMapping, string>>(key: DeleteInput<TaskKind>, options?: DeleteOptions): Promise<Task<TaskKind, TaskMapping[TaskKind]> | undefined>;
|
|
42
|
+
claim<TaskKind extends Extract<keyof TaskMapping, string>>(input: ClaimTaskInput<TaskKind>): Promise<Task<TaskKind, TaskMapping[TaskKind]> | undefined>;
|
|
43
|
+
retry<TaskKind extends keyof TaskMapping>(taskId: string, retryAt: Date): Promise<Task<TaskKind, TaskMapping[TaskKind]>>;
|
|
44
|
+
complete<TaskKind extends keyof TaskMapping>(taskId: string): Promise<Task<TaskKind, TaskMapping[TaskKind]>>;
|
|
45
|
+
fail<TaskKind extends keyof TaskMapping>(taskId: string): Promise<Task<TaskKind, TaskMapping[TaskKind]>>;
|
|
46
|
+
private updateOrThrow;
|
|
47
|
+
private collection;
|
|
48
|
+
private toObject;
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
export { ChronoMongoDatastore, type ChronoMongoDatastoreConfig, type MongoDatastoreOptions };
|
|
52
|
+
//# sourceMappingURL=index.d.ts.map
|
package/build/index.js
CHANGED
|
@@ -1,6 +1,219 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
var
|
|
5
|
-
|
|
1
|
+
//#region rolldown:runtime
|
|
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);
|
|
26
|
+
let mongodb = require("mongodb");
|
|
27
|
+
mongodb = __toESM(mongodb);
|
|
28
|
+
|
|
29
|
+
//#region src/mongo-indexes.ts
|
|
30
|
+
const DEFAULT_EXPIRY_SECONDS = 3600 * 24 * 30;
|
|
31
|
+
const IndexNames = {
|
|
32
|
+
COMPLETED_DOCUMENT_TTL_INDEX: "chrono-completed-document-ttl-index",
|
|
33
|
+
CLAIM_DOCUMENT_INDEX: "chrono-claim-document-index",
|
|
34
|
+
IDEMPOTENCY_KEY_INDEX: "chrono-idempotency-key-index"
|
|
35
|
+
};
|
|
36
|
+
async function ensureIndexes(collection, options) {
|
|
37
|
+
await collection.createIndex({ completedAt: -1 }, {
|
|
38
|
+
partialFilterExpression: {
|
|
39
|
+
completedAt: { $exists: true },
|
|
40
|
+
status: { $eq: __neofinancial_chrono.TaskStatus.COMPLETED }
|
|
41
|
+
},
|
|
42
|
+
expireAfterSeconds: options.expireAfterSeconds || DEFAULT_EXPIRY_SECONDS,
|
|
43
|
+
name: IndexNames.COMPLETED_DOCUMENT_TTL_INDEX
|
|
44
|
+
});
|
|
45
|
+
await collection.createIndex({
|
|
46
|
+
kind: 1,
|
|
47
|
+
status: 1,
|
|
48
|
+
scheduledAt: 1,
|
|
49
|
+
priority: -1,
|
|
50
|
+
claimedAt: 1
|
|
51
|
+
}, { name: IndexNames.CLAIM_DOCUMENT_INDEX });
|
|
52
|
+
await collection.createIndex({ idempotencyKey: 1 }, {
|
|
53
|
+
name: IndexNames.IDEMPOTENCY_KEY_INDEX,
|
|
54
|
+
unique: true,
|
|
55
|
+
sparse: true
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/chrono-mongo-datastore.ts
|
|
61
|
+
const DEFAULT_COLLECTION_NAME = "chrono-tasks";
|
|
62
|
+
var ChronoMongoDatastore = class {
|
|
63
|
+
config;
|
|
64
|
+
database;
|
|
65
|
+
databaseResolvers = [];
|
|
66
|
+
constructor(config) {
|
|
67
|
+
this.config = {
|
|
68
|
+
completedDocumentTTLSeconds: config?.completedDocumentTTLSeconds,
|
|
69
|
+
collectionName: config?.collectionName || DEFAULT_COLLECTION_NAME
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Sets the database connection for the datastore. Ensures that the indexes are created and resolves any pending promises waiting for the database.
|
|
74
|
+
*
|
|
75
|
+
* @param database - The database to set.
|
|
76
|
+
*/
|
|
77
|
+
async initialize(database) {
|
|
78
|
+
if (this.database) throw new Error("Database connection already set");
|
|
79
|
+
await ensureIndexes(database.collection(this.config.collectionName), { expireAfterSeconds: this.config.completedDocumentTTLSeconds });
|
|
80
|
+
this.database = database;
|
|
81
|
+
const resolvers = this.databaseResolvers.splice(0);
|
|
82
|
+
for (const resolve of resolvers) resolve(database);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 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.
|
|
86
|
+
*
|
|
87
|
+
* @returns The database connection.
|
|
88
|
+
*/
|
|
89
|
+
async getDatabase() {
|
|
90
|
+
if (this.database) return this.database;
|
|
91
|
+
return new Promise((resolve) => {
|
|
92
|
+
this.databaseResolvers.push(resolve);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
async schedule(input) {
|
|
96
|
+
const createInput = {
|
|
97
|
+
kind: input.kind,
|
|
98
|
+
status: __neofinancial_chrono.TaskStatus.PENDING,
|
|
99
|
+
data: input.data,
|
|
100
|
+
priority: input.priority,
|
|
101
|
+
idempotencyKey: input.idempotencyKey,
|
|
102
|
+
originalScheduleDate: input.when,
|
|
103
|
+
scheduledAt: input.when,
|
|
104
|
+
retryCount: 0
|
|
105
|
+
};
|
|
106
|
+
try {
|
|
107
|
+
const results = await (await this.getDatabase()).collection(this.config.collectionName).insertOne(createInput, {
|
|
108
|
+
...input?.datastoreOptions?.session ? { session: input.datastoreOptions.session } : void 0,
|
|
109
|
+
ignoreUndefined: true
|
|
110
|
+
});
|
|
111
|
+
if (results.acknowledged) return this.toObject({
|
|
112
|
+
_id: results.insertedId,
|
|
113
|
+
...createInput
|
|
114
|
+
});
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (input.idempotencyKey && error instanceof Error && "code" in error && (error.code === 11e3 || error.code === 11001)) {
|
|
117
|
+
const existingTask = await (await this.collection()).findOne({ idempotencyKey: input.idempotencyKey }, {
|
|
118
|
+
hint: IndexNames.IDEMPOTENCY_KEY_INDEX,
|
|
119
|
+
...input.datastoreOptions?.session ? { session: input.datastoreOptions.session } : void 0
|
|
120
|
+
});
|
|
121
|
+
if (existingTask) return this.toObject(existingTask);
|
|
122
|
+
throw new Error(`Failed to find existing task with idempotency key ${input.idempotencyKey} despite unique index error`);
|
|
123
|
+
}
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
throw new Error(`Failed to insert ${String(input.kind)} document`);
|
|
127
|
+
}
|
|
128
|
+
async delete(key, options) {
|
|
129
|
+
const filter = typeof key === "string" ? { _id: new mongodb.ObjectId(key) } : {
|
|
130
|
+
kind: key.kind,
|
|
131
|
+
idempotencyKey: key.idempotencyKey
|
|
132
|
+
};
|
|
133
|
+
const task = await (await this.collection()).findOneAndDelete({
|
|
134
|
+
...filter,
|
|
135
|
+
...options?.force ? {} : { status: __neofinancial_chrono.TaskStatus.PENDING }
|
|
136
|
+
});
|
|
137
|
+
if (!task) {
|
|
138
|
+
if (options?.force) return;
|
|
139
|
+
const description = typeof key === "string" ? `with id ${key}` : `with kind ${String(key.kind)} and idempotencyKey ${key.idempotencyKey}`;
|
|
140
|
+
throw new Error(`Task ${description} can not be deleted as it may not exist or it's not in PENDING status.`);
|
|
141
|
+
}
|
|
142
|
+
return this.toObject(task);
|
|
143
|
+
}
|
|
144
|
+
async claim(input) {
|
|
145
|
+
const now = /* @__PURE__ */ new Date();
|
|
146
|
+
const task = await (await this.collection()).findOneAndUpdate({
|
|
147
|
+
kind: input.kind,
|
|
148
|
+
scheduledAt: { $lte: now },
|
|
149
|
+
$or: [{ status: __neofinancial_chrono.TaskStatus.PENDING }, {
|
|
150
|
+
status: __neofinancial_chrono.TaskStatus.CLAIMED,
|
|
151
|
+
claimedAt: { $lte: new Date(now.getTime() - input.claimStaleTimeoutMs) }
|
|
152
|
+
}]
|
|
153
|
+
}, { $set: {
|
|
154
|
+
status: __neofinancial_chrono.TaskStatus.CLAIMED,
|
|
155
|
+
claimedAt: now
|
|
156
|
+
} }, {
|
|
157
|
+
sort: {
|
|
158
|
+
priority: -1,
|
|
159
|
+
scheduledAt: 1
|
|
160
|
+
},
|
|
161
|
+
returnDocument: "after"
|
|
162
|
+
});
|
|
163
|
+
return task ? this.toObject(task) : void 0;
|
|
164
|
+
}
|
|
165
|
+
async retry(taskId, retryAt) {
|
|
166
|
+
const taskDocument = await this.updateOrThrow(taskId, {
|
|
167
|
+
$set: {
|
|
168
|
+
status: __neofinancial_chrono.TaskStatus.PENDING,
|
|
169
|
+
scheduledAt: retryAt
|
|
170
|
+
},
|
|
171
|
+
$inc: { retryCount: 1 }
|
|
172
|
+
});
|
|
173
|
+
return this.toObject(taskDocument);
|
|
174
|
+
}
|
|
175
|
+
async complete(taskId) {
|
|
176
|
+
const now = /* @__PURE__ */ new Date();
|
|
177
|
+
const task = await this.updateOrThrow(taskId, { $set: {
|
|
178
|
+
status: __neofinancial_chrono.TaskStatus.COMPLETED,
|
|
179
|
+
completedAt: now,
|
|
180
|
+
lastExecutedAt: now
|
|
181
|
+
} });
|
|
182
|
+
return this.toObject(task);
|
|
183
|
+
}
|
|
184
|
+
async fail(taskId) {
|
|
185
|
+
const now = /* @__PURE__ */ new Date();
|
|
186
|
+
const task = await this.updateOrThrow(taskId, { $set: {
|
|
187
|
+
status: __neofinancial_chrono.TaskStatus.FAILED,
|
|
188
|
+
lastExecutedAt: now
|
|
189
|
+
} });
|
|
190
|
+
return this.toObject(task);
|
|
191
|
+
}
|
|
192
|
+
async updateOrThrow(taskId, update) {
|
|
193
|
+
const document = await (await this.collection()).findOneAndUpdate({ _id: new mongodb.ObjectId(taskId) }, update, { returnDocument: "after" });
|
|
194
|
+
if (!document) throw new Error(`Task with ID ${taskId} not found`);
|
|
195
|
+
return document;
|
|
196
|
+
}
|
|
197
|
+
async collection() {
|
|
198
|
+
return (await this.getDatabase()).collection(this.config.collectionName);
|
|
199
|
+
}
|
|
200
|
+
toObject(document) {
|
|
201
|
+
return {
|
|
202
|
+
id: document._id.toHexString(),
|
|
203
|
+
data: document.data,
|
|
204
|
+
kind: document.kind,
|
|
205
|
+
status: document.status,
|
|
206
|
+
priority: document.priority ?? void 0,
|
|
207
|
+
idempotencyKey: document.idempotencyKey ?? void 0,
|
|
208
|
+
originalScheduleDate: document.originalScheduleDate,
|
|
209
|
+
scheduledAt: document.scheduledAt,
|
|
210
|
+
claimedAt: document.claimedAt ?? void 0,
|
|
211
|
+
completedAt: document.completedAt ?? void 0,
|
|
212
|
+
retryCount: document.retryCount
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
//#endregion
|
|
218
|
+
exports.ChronoMongoDatastore = ChronoMongoDatastore;
|
|
6
219
|
//# sourceMappingURL=index.js.map
|
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,mEAIkC;AAHhC,8HAAA,oBAAoB,OAAA"}
|
|
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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"}
|
package/build/index.mjs
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { TaskStatus } from "@neofinancial/chrono";
|
|
2
|
+
import { ObjectId } from "mongodb";
|
|
3
|
+
|
|
4
|
+
//#region src/mongo-indexes.ts
|
|
5
|
+
const DEFAULT_EXPIRY_SECONDS = 3600 * 24 * 30;
|
|
6
|
+
const IndexNames = {
|
|
7
|
+
COMPLETED_DOCUMENT_TTL_INDEX: "chrono-completed-document-ttl-index",
|
|
8
|
+
CLAIM_DOCUMENT_INDEX: "chrono-claim-document-index",
|
|
9
|
+
IDEMPOTENCY_KEY_INDEX: "chrono-idempotency-key-index"
|
|
10
|
+
};
|
|
11
|
+
async function ensureIndexes(collection, options) {
|
|
12
|
+
await collection.createIndex({ completedAt: -1 }, {
|
|
13
|
+
partialFilterExpression: {
|
|
14
|
+
completedAt: { $exists: true },
|
|
15
|
+
status: { $eq: TaskStatus.COMPLETED }
|
|
16
|
+
},
|
|
17
|
+
expireAfterSeconds: options.expireAfterSeconds || DEFAULT_EXPIRY_SECONDS,
|
|
18
|
+
name: IndexNames.COMPLETED_DOCUMENT_TTL_INDEX
|
|
19
|
+
});
|
|
20
|
+
await collection.createIndex({
|
|
21
|
+
kind: 1,
|
|
22
|
+
status: 1,
|
|
23
|
+
scheduledAt: 1,
|
|
24
|
+
priority: -1,
|
|
25
|
+
claimedAt: 1
|
|
26
|
+
}, { name: IndexNames.CLAIM_DOCUMENT_INDEX });
|
|
27
|
+
await collection.createIndex({ idempotencyKey: 1 }, {
|
|
28
|
+
name: IndexNames.IDEMPOTENCY_KEY_INDEX,
|
|
29
|
+
unique: true,
|
|
30
|
+
sparse: true
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/chrono-mongo-datastore.ts
|
|
36
|
+
const DEFAULT_COLLECTION_NAME = "chrono-tasks";
|
|
37
|
+
var ChronoMongoDatastore = class {
|
|
38
|
+
config;
|
|
39
|
+
database;
|
|
40
|
+
databaseResolvers = [];
|
|
41
|
+
constructor(config) {
|
|
42
|
+
this.config = {
|
|
43
|
+
completedDocumentTTLSeconds: config?.completedDocumentTTLSeconds,
|
|
44
|
+
collectionName: config?.collectionName || DEFAULT_COLLECTION_NAME
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Sets the database connection for the datastore. Ensures that the indexes are created and resolves any pending promises waiting for the database.
|
|
49
|
+
*
|
|
50
|
+
* @param database - The database to set.
|
|
51
|
+
*/
|
|
52
|
+
async initialize(database) {
|
|
53
|
+
if (this.database) throw new Error("Database connection already set");
|
|
54
|
+
await ensureIndexes(database.collection(this.config.collectionName), { expireAfterSeconds: this.config.completedDocumentTTLSeconds });
|
|
55
|
+
this.database = database;
|
|
56
|
+
const resolvers = this.databaseResolvers.splice(0);
|
|
57
|
+
for (const resolve of resolvers) resolve(database);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 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.
|
|
61
|
+
*
|
|
62
|
+
* @returns The database connection.
|
|
63
|
+
*/
|
|
64
|
+
async getDatabase() {
|
|
65
|
+
if (this.database) return this.database;
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
this.databaseResolvers.push(resolve);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
async schedule(input) {
|
|
71
|
+
const createInput = {
|
|
72
|
+
kind: input.kind,
|
|
73
|
+
status: TaskStatus.PENDING,
|
|
74
|
+
data: input.data,
|
|
75
|
+
priority: input.priority,
|
|
76
|
+
idempotencyKey: input.idempotencyKey,
|
|
77
|
+
originalScheduleDate: input.when,
|
|
78
|
+
scheduledAt: input.when,
|
|
79
|
+
retryCount: 0
|
|
80
|
+
};
|
|
81
|
+
try {
|
|
82
|
+
const results = await (await this.getDatabase()).collection(this.config.collectionName).insertOne(createInput, {
|
|
83
|
+
...input?.datastoreOptions?.session ? { session: input.datastoreOptions.session } : void 0,
|
|
84
|
+
ignoreUndefined: true
|
|
85
|
+
});
|
|
86
|
+
if (results.acknowledged) return this.toObject({
|
|
87
|
+
_id: results.insertedId,
|
|
88
|
+
...createInput
|
|
89
|
+
});
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (input.idempotencyKey && error instanceof Error && "code" in error && (error.code === 11e3 || error.code === 11001)) {
|
|
92
|
+
const existingTask = await (await this.collection()).findOne({ idempotencyKey: input.idempotencyKey }, {
|
|
93
|
+
hint: IndexNames.IDEMPOTENCY_KEY_INDEX,
|
|
94
|
+
...input.datastoreOptions?.session ? { session: input.datastoreOptions.session } : void 0
|
|
95
|
+
});
|
|
96
|
+
if (existingTask) return this.toObject(existingTask);
|
|
97
|
+
throw new Error(`Failed to find existing task with idempotency key ${input.idempotencyKey} despite unique index error`);
|
|
98
|
+
}
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
throw new Error(`Failed to insert ${String(input.kind)} document`);
|
|
102
|
+
}
|
|
103
|
+
async delete(key, options) {
|
|
104
|
+
const filter = typeof key === "string" ? { _id: new ObjectId(key) } : {
|
|
105
|
+
kind: key.kind,
|
|
106
|
+
idempotencyKey: key.idempotencyKey
|
|
107
|
+
};
|
|
108
|
+
const task = await (await this.collection()).findOneAndDelete({
|
|
109
|
+
...filter,
|
|
110
|
+
...options?.force ? {} : { status: TaskStatus.PENDING }
|
|
111
|
+
});
|
|
112
|
+
if (!task) {
|
|
113
|
+
if (options?.force) return;
|
|
114
|
+
const description = typeof key === "string" ? `with id ${key}` : `with kind ${String(key.kind)} and idempotencyKey ${key.idempotencyKey}`;
|
|
115
|
+
throw new Error(`Task ${description} can not be deleted as it may not exist or it's not in PENDING status.`);
|
|
116
|
+
}
|
|
117
|
+
return this.toObject(task);
|
|
118
|
+
}
|
|
119
|
+
async claim(input) {
|
|
120
|
+
const now = /* @__PURE__ */ new Date();
|
|
121
|
+
const task = await (await this.collection()).findOneAndUpdate({
|
|
122
|
+
kind: input.kind,
|
|
123
|
+
scheduledAt: { $lte: now },
|
|
124
|
+
$or: [{ status: TaskStatus.PENDING }, {
|
|
125
|
+
status: TaskStatus.CLAIMED,
|
|
126
|
+
claimedAt: { $lte: new Date(now.getTime() - input.claimStaleTimeoutMs) }
|
|
127
|
+
}]
|
|
128
|
+
}, { $set: {
|
|
129
|
+
status: TaskStatus.CLAIMED,
|
|
130
|
+
claimedAt: now
|
|
131
|
+
} }, {
|
|
132
|
+
sort: {
|
|
133
|
+
priority: -1,
|
|
134
|
+
scheduledAt: 1
|
|
135
|
+
},
|
|
136
|
+
returnDocument: "after"
|
|
137
|
+
});
|
|
138
|
+
return task ? this.toObject(task) : void 0;
|
|
139
|
+
}
|
|
140
|
+
async retry(taskId, retryAt) {
|
|
141
|
+
const taskDocument = await this.updateOrThrow(taskId, {
|
|
142
|
+
$set: {
|
|
143
|
+
status: TaskStatus.PENDING,
|
|
144
|
+
scheduledAt: retryAt
|
|
145
|
+
},
|
|
146
|
+
$inc: { retryCount: 1 }
|
|
147
|
+
});
|
|
148
|
+
return this.toObject(taskDocument);
|
|
149
|
+
}
|
|
150
|
+
async complete(taskId) {
|
|
151
|
+
const now = /* @__PURE__ */ new Date();
|
|
152
|
+
const task = await this.updateOrThrow(taskId, { $set: {
|
|
153
|
+
status: TaskStatus.COMPLETED,
|
|
154
|
+
completedAt: now,
|
|
155
|
+
lastExecutedAt: now
|
|
156
|
+
} });
|
|
157
|
+
return this.toObject(task);
|
|
158
|
+
}
|
|
159
|
+
async fail(taskId) {
|
|
160
|
+
const now = /* @__PURE__ */ new Date();
|
|
161
|
+
const task = await this.updateOrThrow(taskId, { $set: {
|
|
162
|
+
status: TaskStatus.FAILED,
|
|
163
|
+
lastExecutedAt: now
|
|
164
|
+
} });
|
|
165
|
+
return this.toObject(task);
|
|
166
|
+
}
|
|
167
|
+
async updateOrThrow(taskId, update) {
|
|
168
|
+
const document = await (await this.collection()).findOneAndUpdate({ _id: new ObjectId(taskId) }, update, { returnDocument: "after" });
|
|
169
|
+
if (!document) throw new Error(`Task with ID ${taskId} not found`);
|
|
170
|
+
return document;
|
|
171
|
+
}
|
|
172
|
+
async collection() {
|
|
173
|
+
return (await this.getDatabase()).collection(this.config.collectionName);
|
|
174
|
+
}
|
|
175
|
+
toObject(document) {
|
|
176
|
+
return {
|
|
177
|
+
id: document._id.toHexString(),
|
|
178
|
+
data: document.data,
|
|
179
|
+
kind: document.kind,
|
|
180
|
+
status: document.status,
|
|
181
|
+
priority: document.priority ?? void 0,
|
|
182
|
+
idempotencyKey: document.idempotencyKey ?? void 0,
|
|
183
|
+
originalScheduleDate: document.originalScheduleDate,
|
|
184
|
+
scheduledAt: document.scheduledAt,
|
|
185
|
+
claimedAt: document.claimedAt ?? void 0,
|
|
186
|
+
completedAt: document.completedAt ?? void 0,
|
|
187
|
+
retryCount: document.retryCount
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
//#endregion
|
|
193
|
+
export { ChronoMongoDatastore };
|
|
194
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neofinancial/chrono-mongo-datastore",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "MongoDB datastore implementation for Chrono task scheduling system",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -11,8 +11,21 @@
|
|
|
11
11
|
"type": "git",
|
|
12
12
|
"url": "https://github.com/neofinancial/chrono.git"
|
|
13
13
|
},
|
|
14
|
-
"main": "build/index.js",
|
|
15
|
-
"
|
|
14
|
+
"main": "./build/index.js",
|
|
15
|
+
"module": "./build/index.mjs",
|
|
16
|
+
"types": "./build/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"import": {
|
|
20
|
+
"types": "./build/index.d.mts",
|
|
21
|
+
"default": "./build/index.mjs"
|
|
22
|
+
},
|
|
23
|
+
"require": {
|
|
24
|
+
"types": "./build/index.d.ts",
|
|
25
|
+
"default": "./build/index.js"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
16
29
|
"keywords": [],
|
|
17
30
|
"author": "Neo Financial Engineering <engineering@neofinancial.com>",
|
|
18
31
|
"license": "MIT",
|
|
@@ -23,15 +36,15 @@
|
|
|
23
36
|
"devDependencies": {
|
|
24
37
|
"mongodb": "^6",
|
|
25
38
|
"mongodb-memory-server": "^10.1.4",
|
|
26
|
-
"@neofinancial/chrono": "0.
|
|
39
|
+
"@neofinancial/chrono": "0.5.0"
|
|
27
40
|
},
|
|
28
41
|
"peerDependencies": {
|
|
29
42
|
"mongodb": "^6",
|
|
30
|
-
"@neofinancial/chrono": "0.
|
|
43
|
+
"@neofinancial/chrono": "0.5.0"
|
|
31
44
|
},
|
|
32
45
|
"scripts": {
|
|
33
46
|
"clean": "rimraf ./build",
|
|
34
|
-
"build": "
|
|
47
|
+
"build": "tsdown",
|
|
35
48
|
"typecheck": "tsc -p ./tsconfig.json --noEmit",
|
|
36
49
|
"test": "NODE_ENV=test TZ=UTC vitest run"
|
|
37
50
|
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { type ClaimTaskInput, type Datastore, type DeleteInput, type DeleteOptions, type ScheduleInput, type Task, type TaskMappingBase } from '@neofinancial/chrono';
|
|
2
|
-
import { type ClientSession, type Db, type WithId } from 'mongodb';
|
|
3
|
-
export type ChronoMongoDatastoreConfig = {
|
|
4
|
-
/**
|
|
5
|
-
* The TTL (in seconds) for completed documents.
|
|
6
|
-
*
|
|
7
|
-
* @default 60 * 60 * 24 * 30 // 30 days
|
|
8
|
-
* @type {number}
|
|
9
|
-
*/
|
|
10
|
-
completedDocumentTTLSeconds?: number;
|
|
11
|
-
/**
|
|
12
|
-
* The name of the collection to use for the datastore.
|
|
13
|
-
*
|
|
14
|
-
* @type {string}
|
|
15
|
-
*/
|
|
16
|
-
collectionName: string;
|
|
17
|
-
};
|
|
18
|
-
export type MongoDatastoreOptions = {
|
|
19
|
-
session?: ClientSession;
|
|
20
|
-
};
|
|
21
|
-
export type TaskDocument<TaskKind, TaskData> = WithId<Omit<Task<TaskKind, TaskData>, 'id'>>;
|
|
22
|
-
export declare class ChronoMongoDatastore<TaskMapping extends TaskMappingBase> implements Datastore<TaskMapping, MongoDatastoreOptions> {
|
|
23
|
-
private config;
|
|
24
|
-
private database;
|
|
25
|
-
private databaseResolvers;
|
|
26
|
-
constructor(config?: Partial<ChronoMongoDatastoreConfig>);
|
|
27
|
-
/**
|
|
28
|
-
* Sets the database connection for the datastore. Ensures that the indexes are created and resolves any pending promises waiting for the database.
|
|
29
|
-
*
|
|
30
|
-
* @param database - The database to set.
|
|
31
|
-
*/
|
|
32
|
-
initialize(database: Db): Promise<void>;
|
|
33
|
-
/**
|
|
34
|
-
* 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.
|
|
35
|
-
*
|
|
36
|
-
* @returns The database connection.
|
|
37
|
-
*/
|
|
38
|
-
getDatabase(): Promise<Db>;
|
|
39
|
-
schedule<TaskKind extends keyof TaskMapping>(input: ScheduleInput<TaskKind, TaskMapping[TaskKind], MongoDatastoreOptions>): Promise<Task<TaskKind, TaskMapping[TaskKind]>>;
|
|
40
|
-
delete<TaskKind extends Extract<keyof TaskMapping, string>>(key: DeleteInput<TaskKind>, options?: DeleteOptions): Promise<Task<TaskKind, TaskMapping[TaskKind]> | undefined>;
|
|
41
|
-
claim<TaskKind extends Extract<keyof TaskMapping, string>>(input: ClaimTaskInput<TaskKind>): Promise<Task<TaskKind, TaskMapping[TaskKind]> | undefined>;
|
|
42
|
-
retry<TaskKind extends keyof TaskMapping>(taskId: string, retryAt: Date): Promise<Task<TaskKind, TaskMapping[TaskKind]>>;
|
|
43
|
-
complete<TaskKind extends keyof TaskMapping>(taskId: string): Promise<Task<TaskKind, TaskMapping[TaskKind]>>;
|
|
44
|
-
fail<TaskKind extends keyof TaskMapping>(taskId: string): Promise<Task<TaskKind, TaskMapping[TaskKind]>>;
|
|
45
|
-
private updateOrThrow;
|
|
46
|
-
private collection;
|
|
47
|
-
private toObject;
|
|
48
|
-
}
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ChronoMongoDatastore = void 0;
|
|
4
|
-
const chrono_1 = require("@neofinancial/chrono");
|
|
5
|
-
const mongodb_1 = require("mongodb");
|
|
6
|
-
const mongo_indexes_1 = require("./mongo-indexes");
|
|
7
|
-
const DEFAULT_COLLECTION_NAME = 'chrono-tasks';
|
|
8
|
-
class ChronoMongoDatastore {
|
|
9
|
-
config;
|
|
10
|
-
database;
|
|
11
|
-
databaseResolvers = [];
|
|
12
|
-
constructor(config) {
|
|
13
|
-
this.config = {
|
|
14
|
-
completedDocumentTTLSeconds: config?.completedDocumentTTLSeconds,
|
|
15
|
-
collectionName: config?.collectionName || DEFAULT_COLLECTION_NAME,
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Sets the database connection for the datastore. Ensures that the indexes are created and resolves any pending promises waiting for the database.
|
|
20
|
-
*
|
|
21
|
-
* @param database - The database to set.
|
|
22
|
-
*/
|
|
23
|
-
async initialize(database) {
|
|
24
|
-
if (this.database) {
|
|
25
|
-
throw new Error('Database connection already set');
|
|
26
|
-
}
|
|
27
|
-
await (0, mongo_indexes_1.ensureIndexes)(database.collection(this.config.collectionName), {
|
|
28
|
-
expireAfterSeconds: this.config.completedDocumentTTLSeconds,
|
|
29
|
-
});
|
|
30
|
-
this.database = database;
|
|
31
|
-
const resolvers = this.databaseResolvers.splice(0);
|
|
32
|
-
for (const resolve of resolvers) {
|
|
33
|
-
resolve(database);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* 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.
|
|
38
|
-
*
|
|
39
|
-
* @returns The database connection.
|
|
40
|
-
*/
|
|
41
|
-
async getDatabase() {
|
|
42
|
-
if (this.database) {
|
|
43
|
-
return this.database;
|
|
44
|
-
}
|
|
45
|
-
return new Promise((resolve) => {
|
|
46
|
-
this.databaseResolvers.push(resolve);
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
async schedule(input) {
|
|
50
|
-
const createInput = {
|
|
51
|
-
kind: input.kind,
|
|
52
|
-
status: chrono_1.TaskStatus.PENDING,
|
|
53
|
-
data: input.data,
|
|
54
|
-
priority: input.priority,
|
|
55
|
-
idempotencyKey: input.idempotencyKey,
|
|
56
|
-
originalScheduleDate: input.when,
|
|
57
|
-
scheduledAt: input.when,
|
|
58
|
-
retryCount: 0,
|
|
59
|
-
};
|
|
60
|
-
try {
|
|
61
|
-
const database = await this.getDatabase();
|
|
62
|
-
const results = await database.collection(this.config.collectionName).insertOne(createInput, {
|
|
63
|
-
...(input?.datastoreOptions?.session ? { session: input.datastoreOptions.session } : undefined),
|
|
64
|
-
ignoreUndefined: true,
|
|
65
|
-
});
|
|
66
|
-
if (results.acknowledged) {
|
|
67
|
-
return this.toObject({ _id: results.insertedId, ...createInput });
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
catch (error) {
|
|
71
|
-
if (input.idempotencyKey &&
|
|
72
|
-
error instanceof Error &&
|
|
73
|
-
'code' in error &&
|
|
74
|
-
(error.code === 11000 || error.code === 11001)) {
|
|
75
|
-
const collection = await this.collection();
|
|
76
|
-
const existingTask = await collection.findOne({
|
|
77
|
-
idempotencyKey: input.idempotencyKey,
|
|
78
|
-
}, {
|
|
79
|
-
hint: mongo_indexes_1.IndexNames.IDEMPOTENCY_KEY_INDEX,
|
|
80
|
-
...(input.datastoreOptions?.session ? { session: input.datastoreOptions.session } : undefined),
|
|
81
|
-
});
|
|
82
|
-
if (existingTask) {
|
|
83
|
-
return this.toObject(existingTask);
|
|
84
|
-
}
|
|
85
|
-
throw new Error(`Failed to find existing task with idempotency key ${input.idempotencyKey} despite unique index error`);
|
|
86
|
-
}
|
|
87
|
-
throw error;
|
|
88
|
-
}
|
|
89
|
-
throw new Error(`Failed to insert ${String(input.kind)} document`);
|
|
90
|
-
}
|
|
91
|
-
async delete(key, options) {
|
|
92
|
-
const filter = typeof key === 'string' ? { _id: new mongodb_1.ObjectId(key) } : { kind: key.kind, idempotencyKey: key.idempotencyKey };
|
|
93
|
-
const collection = await this.collection();
|
|
94
|
-
const task = await collection.findOneAndDelete({
|
|
95
|
-
...filter,
|
|
96
|
-
...(options?.force ? {} : { status: chrono_1.TaskStatus.PENDING }),
|
|
97
|
-
});
|
|
98
|
-
if (!task) {
|
|
99
|
-
if (options?.force) {
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
const description = typeof key === 'string'
|
|
103
|
-
? `with id ${key}`
|
|
104
|
-
: `with kind ${String(key.kind)} and idempotencyKey ${key.idempotencyKey}`;
|
|
105
|
-
throw new Error(`Task ${description} can not be deleted as it may not exist or it's not in PENDING status.`);
|
|
106
|
-
}
|
|
107
|
-
return this.toObject(task);
|
|
108
|
-
}
|
|
109
|
-
async claim(input) {
|
|
110
|
-
const now = new Date();
|
|
111
|
-
const collection = await this.collection();
|
|
112
|
-
const task = await collection.findOneAndUpdate({
|
|
113
|
-
kind: input.kind,
|
|
114
|
-
scheduledAt: { $lte: now },
|
|
115
|
-
$or: [
|
|
116
|
-
{ status: chrono_1.TaskStatus.PENDING },
|
|
117
|
-
{
|
|
118
|
-
status: chrono_1.TaskStatus.CLAIMED,
|
|
119
|
-
claimedAt: {
|
|
120
|
-
$lte: new Date(now.getTime() - input.claimStaleTimeoutMs),
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
],
|
|
124
|
-
}, { $set: { status: chrono_1.TaskStatus.CLAIMED, claimedAt: now } }, {
|
|
125
|
-
sort: { priority: -1, scheduledAt: 1 },
|
|
126
|
-
// hint: IndexNames.CLAIM_DOCUMENT_INDEX as unknown as Document,
|
|
127
|
-
returnDocument: 'after',
|
|
128
|
-
});
|
|
129
|
-
return task ? this.toObject(task) : undefined;
|
|
130
|
-
}
|
|
131
|
-
async retry(taskId, retryAt) {
|
|
132
|
-
const taskDocument = await this.updateOrThrow(taskId, {
|
|
133
|
-
$set: {
|
|
134
|
-
status: chrono_1.TaskStatus.PENDING,
|
|
135
|
-
scheduledAt: retryAt,
|
|
136
|
-
},
|
|
137
|
-
$inc: {
|
|
138
|
-
retryCount: 1,
|
|
139
|
-
},
|
|
140
|
-
});
|
|
141
|
-
return this.toObject(taskDocument);
|
|
142
|
-
}
|
|
143
|
-
async complete(taskId) {
|
|
144
|
-
const now = new Date();
|
|
145
|
-
const task = await this.updateOrThrow(taskId, {
|
|
146
|
-
$set: {
|
|
147
|
-
status: chrono_1.TaskStatus.COMPLETED,
|
|
148
|
-
completedAt: now,
|
|
149
|
-
lastExecutedAt: now,
|
|
150
|
-
},
|
|
151
|
-
});
|
|
152
|
-
return this.toObject(task);
|
|
153
|
-
}
|
|
154
|
-
async fail(taskId) {
|
|
155
|
-
const now = new Date();
|
|
156
|
-
const task = await this.updateOrThrow(taskId, {
|
|
157
|
-
$set: {
|
|
158
|
-
status: chrono_1.TaskStatus.FAILED,
|
|
159
|
-
lastExecutedAt: now,
|
|
160
|
-
},
|
|
161
|
-
});
|
|
162
|
-
return this.toObject(task);
|
|
163
|
-
}
|
|
164
|
-
async updateOrThrow(taskId, update) {
|
|
165
|
-
const collection = await this.collection();
|
|
166
|
-
const document = await collection.findOneAndUpdate({ _id: new mongodb_1.ObjectId(taskId) }, update, {
|
|
167
|
-
returnDocument: 'after',
|
|
168
|
-
});
|
|
169
|
-
if (!document) {
|
|
170
|
-
throw new Error(`Task with ID ${taskId} not found`);
|
|
171
|
-
}
|
|
172
|
-
return document;
|
|
173
|
-
}
|
|
174
|
-
async collection() {
|
|
175
|
-
const database = await this.getDatabase();
|
|
176
|
-
return database.collection(this.config.collectionName);
|
|
177
|
-
}
|
|
178
|
-
toObject(document) {
|
|
179
|
-
return {
|
|
180
|
-
id: document._id.toHexString(),
|
|
181
|
-
data: document.data,
|
|
182
|
-
kind: document.kind,
|
|
183
|
-
status: document.status,
|
|
184
|
-
priority: document.priority ?? undefined,
|
|
185
|
-
idempotencyKey: document.idempotencyKey ?? undefined,
|
|
186
|
-
originalScheduleDate: document.originalScheduleDate,
|
|
187
|
-
scheduledAt: document.scheduledAt,
|
|
188
|
-
claimedAt: document.claimedAt ?? undefined,
|
|
189
|
-
completedAt: document.completedAt ?? undefined,
|
|
190
|
-
retryCount: document.retryCount,
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
exports.ChronoMongoDatastore = ChronoMongoDatastore;
|
|
195
|
-
//# sourceMappingURL=chrono-mongo-datastore.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"chrono-mongo-datastore.js","sourceRoot":"","sources":["../src/chrono-mongo-datastore.ts"],"names":[],"mappings":"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"}
|
package/build/main.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/build/main.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const chrono_core_1 = require("@neofinancial/chrono-core");
|
|
4
|
-
const mongo_task_1 = require("./mongo-task");
|
|
5
|
-
async function main() {
|
|
6
|
-
const task = new mongo_task_1.MongoTask();
|
|
7
|
-
const scheduler = new chrono_core_1.Scheduler();
|
|
8
|
-
await scheduler.schedule(task);
|
|
9
|
-
await scheduler.run();
|
|
10
|
-
console.log('Successfully ran MongoTask!');
|
|
11
|
-
}
|
|
12
|
-
main().catch(console.error);
|
|
13
|
-
//# sourceMappingURL=main.js.map
|
package/build/main.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";;AAAA,2DAAsD;AAEtD,6CAAyC;AAEzC,KAAK,UAAU,IAAI;IACjB,MAAM,IAAI,GAAG,IAAI,sBAAS,EAAE,CAAC;IAC7B,MAAM,SAAS,GAAG,IAAI,uBAAS,EAAE,CAAC;IAElC,MAAM,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,SAAS,CAAC,GAAG,EAAE,CAAC;IAEtB,OAAO,CAAC,GAAG,CAAC,6BAA6B,CAAC,CAAC;AAC7C,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC"}
|
package/build/mongo-indexes.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { Collection } from 'mongodb';
|
|
2
|
-
export declare const DEFAULT_EXPIRY_SECONDS: number;
|
|
3
|
-
export declare const IndexNames: {
|
|
4
|
-
COMPLETED_DOCUMENT_TTL_INDEX: string;
|
|
5
|
-
CLAIM_DOCUMENT_INDEX: string;
|
|
6
|
-
IDEMPOTENCY_KEY_INDEX: string;
|
|
7
|
-
};
|
|
8
|
-
export type IndexDefinitionOptions = {
|
|
9
|
-
expireAfterSeconds?: number;
|
|
10
|
-
};
|
|
11
|
-
export declare function ensureIndexes(collection: Collection, options: IndexDefinitionOptions): Promise<void>;
|
package/build/mongo-indexes.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.IndexNames = exports.DEFAULT_EXPIRY_SECONDS = void 0;
|
|
4
|
-
exports.ensureIndexes = ensureIndexes;
|
|
5
|
-
const chrono_1 = require("@neofinancial/chrono");
|
|
6
|
-
exports.DEFAULT_EXPIRY_SECONDS = 60 * 60 * 24 * 30; // 30 days
|
|
7
|
-
exports.IndexNames = {
|
|
8
|
-
COMPLETED_DOCUMENT_TTL_INDEX: 'chrono-completed-document-ttl-index',
|
|
9
|
-
CLAIM_DOCUMENT_INDEX: 'chrono-claim-document-index',
|
|
10
|
-
IDEMPOTENCY_KEY_INDEX: 'chrono-idempotency-key-index',
|
|
11
|
-
};
|
|
12
|
-
async function ensureIndexes(collection, options) {
|
|
13
|
-
await collection.createIndex({ completedAt: -1 }, {
|
|
14
|
-
partialFilterExpression: {
|
|
15
|
-
completedAt: { $exists: true },
|
|
16
|
-
status: { $eq: chrono_1.TaskStatus.COMPLETED },
|
|
17
|
-
},
|
|
18
|
-
expireAfterSeconds: options.expireAfterSeconds || exports.DEFAULT_EXPIRY_SECONDS,
|
|
19
|
-
name: exports.IndexNames.COMPLETED_DOCUMENT_TTL_INDEX,
|
|
20
|
-
});
|
|
21
|
-
await collection.createIndex({ kind: 1, status: 1, scheduledAt: 1, priority: -1, claimedAt: 1 }, { name: exports.IndexNames.CLAIM_DOCUMENT_INDEX });
|
|
22
|
-
await collection.createIndex({ idempotencyKey: 1 }, { name: exports.IndexNames.IDEMPOTENCY_KEY_INDEX, unique: true, sparse: true });
|
|
23
|
-
}
|
|
24
|
-
//# sourceMappingURL=mongo-indexes.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"mongo-indexes.js","sourceRoot":"","sources":["../src/mongo-indexes.ts"],"names":[],"mappings":";;;AAeA,sCAsBC;AArCD,iDAAkD;AAGrC,QAAA,sBAAsB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,UAAU;AAEtD,QAAA,UAAU,GAAG;IACxB,4BAA4B,EAAE,qCAAqC;IACnE,oBAAoB,EAAE,6BAA6B;IACnD,qBAAqB,EAAE,8BAA8B;CACtD,CAAC;AAMK,KAAK,UAAU,aAAa,CAAC,UAAsB,EAAE,OAA+B;IACzF,MAAM,UAAU,CAAC,WAAW,CAC1B,EAAE,WAAW,EAAE,CAAC,CAAC,EAAE,EACnB;QACE,uBAAuB,EAAE;YACvB,WAAW,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE;YAC9B,MAAM,EAAE,EAAE,GAAG,EAAE,mBAAU,CAAC,SAAS,EAAE;SACtC;QACD,kBAAkB,EAAE,OAAO,CAAC,kBAAkB,IAAI,8BAAsB;QACxE,IAAI,EAAE,kBAAU,CAAC,4BAA4B;KAC9C,CACF,CAAC;IAEF,MAAM,UAAU,CAAC,WAAW,CAC1B,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,EAClE,EAAE,IAAI,EAAE,kBAAU,CAAC,oBAAoB,EAAE,CAC1C,CAAC;IAEF,MAAM,UAAU,CAAC,WAAW,CAC1B,EAAE,cAAc,EAAE,CAAC,EAAE,EACrB,EAAE,IAAI,EAAE,kBAAU,CAAC,qBAAqB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CACvE,CAAC;AACJ,CAAC"}
|
package/build/mongo-task.d.ts
DELETED
package/build/mongo-task.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MongoTask = void 0;
|
|
4
|
-
class MongoTask {
|
|
5
|
-
async run() {
|
|
6
|
-
console.log('Running MongoTask');
|
|
7
|
-
return true;
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
exports.MongoTask = MongoTask;
|
|
11
|
-
//# sourceMappingURL=mongo-task.js.map
|
package/build/mongo-task.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"mongo-task.js","sourceRoot":"","sources":["../src/mongo-task.ts"],"names":[],"mappings":";;;AAEA,MAAa,SAAS;IACb,KAAK,CAAC,GAAG;QACd,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;QAEjC,OAAO,IAAI,CAAC;IACd,CAAC;CACF;AAND,8BAMC"}
|