@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.
@@ -1,30 +1,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);
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: __neofinancial_chrono.TaskStatus.COMPLETED }
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: __neofinancial_chrono.TaskStatus.PENDING,
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: __neofinancial_chrono.TaskStatus.PENDING }
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: __neofinancial_chrono.TaskStatus.PENDING }, {
150
- status: __neofinancial_chrono.TaskStatus.CLAIMED,
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: __neofinancial_chrono.TaskStatus.CLAIMED,
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: __neofinancial_chrono.TaskStatus.PENDING,
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: __neofinancial_chrono.TaskStatus.COMPLETED,
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: __neofinancial_chrono.TaskStatus.FAILED,
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.js.map
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
- * The TTL (in seconds) for completed documents.
8
- *
9
- * @default 60 * 60 * 24 * 30 // 30 days
10
- * @type {number}
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
- * The name of the collection to use for the datastore.
15
- *
16
- * @type {string}
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
- * 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
- */
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
- * 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
- */
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.ts.map
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
- * The TTL (in seconds) for completed documents.
8
- *
9
- * @default 60 * 60 * 24 * 30 // 30 days
10
- * @type {number}
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
- * The name of the collection to use for the datastore.
15
- *
16
- * @type {string}
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
- * 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
- */
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
- * 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
- */
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>;
@@ -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.1",
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
- "keywords": [],
30
- "author": "Neo Financial Engineering <engineering@neofinancial.com>",
31
- "license": "MIT",
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.1"
41
+ "@neofinancial/chrono": "0.5.2"
40
42
  },
41
43
  "peerDependencies": {
42
- "mongodb": "^6",
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
  }
@@ -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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"}