diff --git a/dist/common/AsyncQueue.cjs b/dist/common/AsyncQueue.cjs new file mode 100644 index 0000000..17c84f8 --- /dev/null +++ b/dist/common/AsyncQueue.cjs @@ -0,0 +1,96 @@ +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// src/common/AsyncQueue.ts +var AsyncQueue_exports = {}; +__export(AsyncQueue_exports, { + createAsyncQueue: () => createAsyncQueue, + createAsyncQueues: () => createAsyncQueues +}); +module.exports = __toCommonJS(AsyncQueue_exports); +function createAsyncQueue(opts = { dedupeConcurrent: false }) { + const { dedupeConcurrent } = opts; + let queue = []; + let running; + let nextPromise = new DeferredPromise(); + const push = (task) => { + let taskPromise = new DeferredPromise(); + if (dedupeConcurrent) { + queue = []; + if (nextPromise.started) + nextPromise = new DeferredPromise(); + taskPromise = nextPromise; + } + queue.push(() => { + taskPromise.started = true; + task().then(taskPromise.resolve).catch(taskPromise.reject); + return taskPromise.promise; + }); + if (!running) + running = start(); + return taskPromise.promise; + }; + const start = async () => { + while (queue.length) { + const task = queue.shift(); + await task().catch(() => { + }); + } + running = void 0; + }; + return { + push, + flush: () => running || Promise.resolve(), + get size() { + return queue.length; + } + }; +} +var createAsyncQueues = (opts = { dedupeConcurrent: false }) => { + const queues = {}; + const push = (queueId, task) => { + if (!queues[queueId]) + queues[queueId] = createAsyncQueue(opts); + return queues[queueId].push(task); + }; + const flush = (queueId) => { + if (!queues[queueId]) + queues[queueId] = createAsyncQueue(opts); + return queues[queueId].flush(); + }; + return { push, flush }; +}; +var DeferredPromise = class { + constructor() { + this.started = false; + this.resolve = () => { + }; + this.reject = () => { + }; + this.promise = new Promise((res, rej) => { + this.resolve = res; + this.reject = rej; + }); + } +}; +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + createAsyncQueue, + createAsyncQueues +}); +//# sourceMappingURL=AsyncQueue.cjs.map \ No newline at end of file diff --git a/dist/common/AsyncQueue.cjs.map b/dist/common/AsyncQueue.cjs.map new file mode 100644 index 0000000..8707e1b --- /dev/null +++ b/dist/common/AsyncQueue.cjs.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../src/common/AsyncQueue.ts"],"sourcesContent":["type Callback = () => Promise\n\nexport type AsyncQueue = {\n push: (task: Callback) => Promise\n flush: () => Promise\n size: number\n}\n\n/**\n * Ensures that each callback pushed onto the queue is executed in series.\n * Such a quetie 😻\n * @param opts.dedupeConcurrent If dedupeConcurrent is `true` it ensures that if multiple\n * tasks are pushed onto the queue while there is an active task, only the\n * last one will be executed, once the active task has completed.\n * e.g. in the below example, only 0 and 3 will be executed.\n * ```\n * const queue = createAsyncQueue({ dedupeConcurrent: true })\n * queue.push(async () => console.log(0)) // returns 0\n * queue.push(async () => console.log(1)) // returns 3\n * queue.push(async () => console.log(2)) // returns 3\n * queue.push(async () => console.log(3)) // returns 3\n * ```\n * */\nexport function createAsyncQueue(opts = { dedupeConcurrent: false }): AsyncQueue {\n const { dedupeConcurrent } = opts\n let queue: Callback[] = []\n let running: Promise | undefined\n let nextPromise = new DeferredPromise()\n const push = (task: Callback) => {\n let taskPromise = new DeferredPromise()\n if (dedupeConcurrent) {\n queue = []\n if (nextPromise.started) nextPromise = new DeferredPromise()\n taskPromise = nextPromise\n }\n queue.push(() => {\n taskPromise.started = true\n task().then(taskPromise.resolve).catch(taskPromise.reject)\n return taskPromise.promise\n })\n if (!running) running = start()\n return taskPromise.promise\n }\n const start = async () => {\n while (queue.length) {\n const task = queue.shift()!\n await task().catch(() => {})\n }\n running = undefined\n }\n return {\n push,\n flush: () => running || Promise.resolve(),\n get size() {\n return queue.length\n },\n }\n}\n\nexport const createAsyncQueues = (opts = { dedupeConcurrent: false }) => {\n const queues: { [queueId: string]: AsyncQueue } = {}\n const push = (queueId: string, task: Callback) => {\n if (!queues[queueId]) queues[queueId] = createAsyncQueue(opts)\n return queues[queueId].push(task)\n }\n const flush = (queueId: string) => {\n if (!queues[queueId]) queues[queueId] = createAsyncQueue(opts)\n return queues[queueId].flush()\n }\n return { push, flush }\n}\n\nclass DeferredPromise {\n started = false\n resolve: (x: T | PromiseLike) => void = () => {}\n reject: (x: E) => void = () => {}\n promise: Promise\n\n constructor() {\n this.promise = new Promise((res, rej) => {\n this.resolve = res\n this.reject = rej\n })\n }\n}\n\n// function main() {\n// const queue = createAsyncQueue()\n// queue.push(async () => {\n// console.log(0)\n// }) // returns 0\n// queue.push(async () => {\n// console.log(1)\n\n// return new Promise((resolve, reject) => {\n// setTimeout(() => {\n// console.log('12')\n// resolve()\n// }, 1000)\n// })\n// }) // returns 3\n// queue.push(async () => console.log(2)) // returns 3\n// queue.push(async () => console.log(3)) // returns 3\n// console.log('hi')\n// }\n\n// main()\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuBO,SAAS,iBAA2B,OAAO,EAAE,kBAAkB,MAAM,GAAkB;AAC5F,QAAM,EAAE,iBAAiB,IAAI;AAC7B,MAAI,QAAuB,CAAC;AAC5B,MAAI;AACJ,MAAI,cAAc,IAAI,gBAAmB;AACzC,QAAM,OAAO,CAAC,SAAsB;AAClC,QAAI,cAAc,IAAI,gBAAmB;AACzC,QAAI,kBAAkB;AACpB,cAAQ,CAAC;AACT,UAAI,YAAY;AAAS,sBAAc,IAAI,gBAAmB;AAC9D,oBAAc;AAAA,IAChB;AACA,UAAM,KAAK,MAAM;AACf,kBAAY,UAAU;AACtB,WAAK,EAAE,KAAK,YAAY,OAAO,EAAE,MAAM,YAAY,MAAM;AACzD,aAAO,YAAY;AAAA,IACrB,CAAC;AACD,QAAI,CAAC;AAAS,gBAAU,MAAM;AAC9B,WAAO,YAAY;AAAA,EACrB;AACA,QAAM,QAAQ,YAAY;AACxB,WAAO,MAAM,QAAQ;AACnB,YAAM,OAAO,MAAM,MAAM;AACzB,YAAM,KAAK,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AAAA,IAC7B;AACA,cAAU;AAAA,EACZ;AACA,SAAO;AAAA,IACL;AAAA,IACA,OAAO,MAAM,WAAW,QAAQ,QAAQ;AAAA,IACxC,IAAI,OAAO;AACT,aAAO,MAAM;AAAA,IACf;AAAA,EACF;AACF;AAEO,IAAM,oBAAoB,CAAW,OAAO,EAAE,kBAAkB,MAAM,MAAM;AACjF,QAAM,SAA+C,CAAC;AACtD,QAAM,OAAO,CAAC,SAAiB,SAAsB;AACnD,QAAI,CAAC,OAAO,OAAO;AAAG,aAAO,OAAO,IAAI,iBAAoB,IAAI;AAChE,WAAO,OAAO,OAAO,EAAE,KAAK,IAAI;AAAA,EAClC;AACA,QAAM,QAAQ,CAAC,YAAoB;AACjC,QAAI,CAAC,OAAO,OAAO;AAAG,aAAO,OAAO,IAAI,iBAAoB,IAAI;AAChE,WAAO,OAAO,OAAO,EAAE,MAAM;AAAA,EAC/B;AACA,SAAO,EAAE,MAAM,MAAM;AACvB;AAEA,IAAM,kBAAN,MAAyC;AAAA,EAMvC,cAAc;AALd,mBAAU;AACV,mBAA2C,MAAM;AAAA,IAAC;AAClD,kBAAyB,MAAM;AAAA,IAAC;AAI9B,SAAK,UAAU,IAAI,QAAW,CAAC,KAAK,QAAQ;AAC1C,WAAK,UAAU;AACf,WAAK,SAAS;AAAA,IAChB,CAAC;AAAA,EACH;AACF;","names":[]} \ No newline at end of file diff --git a/dist/common/AsyncQueue.d.cts b/dist/common/AsyncQueue.d.cts new file mode 100644 index 0000000..5962330 --- /dev/null +++ b/dist/common/AsyncQueue.d.cts @@ -0,0 +1,32 @@ +type Callback = () => Promise; +type AsyncQueue = { + push: (task: Callback) => Promise; + flush: () => Promise; + size: number; +}; +/** + * Ensures that each callback pushed onto the queue is executed in series. + * Such a quetie 😻 + * @param opts.dedupeConcurrent If dedupeConcurrent is `true` it ensures that if multiple + * tasks are pushed onto the queue while there is an active task, only the + * last one will be executed, once the active task has completed. + * e.g. in the below example, only 0 and 3 will be executed. + * ``` + * const queue = createAsyncQueue({ dedupeConcurrent: true }) + * queue.push(async () => console.log(0)) // returns 0 + * queue.push(async () => console.log(1)) // returns 3 + * queue.push(async () => console.log(2)) // returns 3 + * queue.push(async () => console.log(3)) // returns 3 + * ``` + * */ +declare function createAsyncQueue(opts?: { + dedupeConcurrent: boolean; +}): AsyncQueue; +declare const createAsyncQueues: (opts?: { + dedupeConcurrent: boolean; +}) => { + push: (queueId: string, task: Callback) => Promise; + flush: (queueId: string) => Promise; +}; + +export { type AsyncQueue, createAsyncQueue, createAsyncQueues }; diff --git a/dist/common/AsyncQueue.d.ts b/dist/common/AsyncQueue.d.ts new file mode 100644 index 0000000..5962330 --- /dev/null +++ b/dist/common/AsyncQueue.d.ts @@ -0,0 +1,32 @@ +type Callback = () => Promise; +type AsyncQueue = { + push: (task: Callback) => Promise; + flush: () => Promise; + size: number; +}; +/** + * Ensures that each callback pushed onto the queue is executed in series. + * Such a quetie 😻 + * @param opts.dedupeConcurrent If dedupeConcurrent is `true` it ensures that if multiple + * tasks are pushed onto the queue while there is an active task, only the + * last one will be executed, once the active task has completed. + * e.g. in the below example, only 0 and 3 will be executed. + * ``` + * const queue = createAsyncQueue({ dedupeConcurrent: true }) + * queue.push(async () => console.log(0)) // returns 0 + * queue.push(async () => console.log(1)) // returns 3 + * queue.push(async () => console.log(2)) // returns 3 + * queue.push(async () => console.log(3)) // returns 3 + * ``` + * */ +declare function createAsyncQueue(opts?: { + dedupeConcurrent: boolean; +}): AsyncQueue; +declare const createAsyncQueues: (opts?: { + dedupeConcurrent: boolean; +}) => { + push: (queueId: string, task: Callback) => Promise; + flush: (queueId: string) => Promise; +}; + +export { type AsyncQueue, createAsyncQueue, createAsyncQueues }; diff --git a/dist/common/AsyncQueue.js b/dist/common/AsyncQueue.js new file mode 100644 index 0000000..33683b7 --- /dev/null +++ b/dist/common/AsyncQueue.js @@ -0,0 +1,71 @@ +// src/common/AsyncQueue.ts +function createAsyncQueue(opts = { dedupeConcurrent: false }) { + const { dedupeConcurrent } = opts; + let queue = []; + let running; + let nextPromise = new DeferredPromise(); + const push = (task) => { + let taskPromise = new DeferredPromise(); + if (dedupeConcurrent) { + queue = []; + if (nextPromise.started) + nextPromise = new DeferredPromise(); + taskPromise = nextPromise; + } + queue.push(() => { + taskPromise.started = true; + task().then(taskPromise.resolve).catch(taskPromise.reject); + return taskPromise.promise; + }); + if (!running) + running = start(); + return taskPromise.promise; + }; + const start = async () => { + while (queue.length) { + const task = queue.shift(); + await task().catch(() => { + }); + } + running = void 0; + }; + return { + push, + flush: () => running || Promise.resolve(), + get size() { + return queue.length; + } + }; +} +var createAsyncQueues = (opts = { dedupeConcurrent: false }) => { + const queues = {}; + const push = (queueId, task) => { + if (!queues[queueId]) + queues[queueId] = createAsyncQueue(opts); + return queues[queueId].push(task); + }; + const flush = (queueId) => { + if (!queues[queueId]) + queues[queueId] = createAsyncQueue(opts); + return queues[queueId].flush(); + }; + return { push, flush }; +}; +var DeferredPromise = class { + constructor() { + this.started = false; + this.resolve = () => { + }; + this.reject = () => { + }; + this.promise = new Promise((res, rej) => { + this.resolve = res; + this.reject = rej; + }); + } +}; +export { + createAsyncQueue, + createAsyncQueues +}; +//# sourceMappingURL=AsyncQueue.js.map \ No newline at end of file diff --git a/dist/common/AsyncQueue.js.map b/dist/common/AsyncQueue.js.map new file mode 100644 index 0000000..00f03fc --- /dev/null +++ b/dist/common/AsyncQueue.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../src/common/AsyncQueue.ts"],"sourcesContent":["type Callback = () => Promise\n\nexport type AsyncQueue = {\n push: (task: Callback) => Promise\n flush: () => Promise\n size: number\n}\n\n/**\n * Ensures that each callback pushed onto the queue is executed in series.\n * Such a quetie 😻\n * @param opts.dedupeConcurrent If dedupeConcurrent is `true` it ensures that if multiple\n * tasks are pushed onto the queue while there is an active task, only the\n * last one will be executed, once the active task has completed.\n * e.g. in the below example, only 0 and 3 will be executed.\n * ```\n * const queue = createAsyncQueue({ dedupeConcurrent: true })\n * queue.push(async () => console.log(0)) // returns 0\n * queue.push(async () => console.log(1)) // returns 3\n * queue.push(async () => console.log(2)) // returns 3\n * queue.push(async () => console.log(3)) // returns 3\n * ```\n * */\nexport function createAsyncQueue(opts = { dedupeConcurrent: false }): AsyncQueue {\n const { dedupeConcurrent } = opts\n let queue: Callback[] = []\n let running: Promise | undefined\n let nextPromise = new DeferredPromise()\n const push = (task: Callback) => {\n let taskPromise = new DeferredPromise()\n if (dedupeConcurrent) {\n queue = []\n if (nextPromise.started) nextPromise = new DeferredPromise()\n taskPromise = nextPromise\n }\n queue.push(() => {\n taskPromise.started = true\n task().then(taskPromise.resolve).catch(taskPromise.reject)\n return taskPromise.promise\n })\n if (!running) running = start()\n return taskPromise.promise\n }\n const start = async () => {\n while (queue.length) {\n const task = queue.shift()!\n await task().catch(() => {})\n }\n running = undefined\n }\n return {\n push,\n flush: () => running || Promise.resolve(),\n get size() {\n return queue.length\n },\n }\n}\n\nexport const createAsyncQueues = (opts = { dedupeConcurrent: false }) => {\n const queues: { [queueId: string]: AsyncQueue } = {}\n const push = (queueId: string, task: Callback) => {\n if (!queues[queueId]) queues[queueId] = createAsyncQueue(opts)\n return queues[queueId].push(task)\n }\n const flush = (queueId: string) => {\n if (!queues[queueId]) queues[queueId] = createAsyncQueue(opts)\n return queues[queueId].flush()\n }\n return { push, flush }\n}\n\nclass DeferredPromise {\n started = false\n resolve: (x: T | PromiseLike) => void = () => {}\n reject: (x: E) => void = () => {}\n promise: Promise\n\n constructor() {\n this.promise = new Promise((res, rej) => {\n this.resolve = res\n this.reject = rej\n })\n }\n}\n\n// function main() {\n// const queue = createAsyncQueue()\n// queue.push(async () => {\n// console.log(0)\n// }) // returns 0\n// queue.push(async () => {\n// console.log(1)\n\n// return new Promise((resolve, reject) => {\n// setTimeout(() => {\n// console.log('12')\n// resolve()\n// }, 1000)\n// })\n// }) // returns 3\n// queue.push(async () => console.log(2)) // returns 3\n// queue.push(async () => console.log(3)) // returns 3\n// console.log('hi')\n// }\n\n// main()\n"],"mappings":";AAuBO,SAAS,iBAA2B,OAAO,EAAE,kBAAkB,MAAM,GAAkB;AAC5F,QAAM,EAAE,iBAAiB,IAAI;AAC7B,MAAI,QAAuB,CAAC;AAC5B,MAAI;AACJ,MAAI,cAAc,IAAI,gBAAmB;AACzC,QAAM,OAAO,CAAC,SAAsB;AAClC,QAAI,cAAc,IAAI,gBAAmB;AACzC,QAAI,kBAAkB;AACpB,cAAQ,CAAC;AACT,UAAI,YAAY;AAAS,sBAAc,IAAI,gBAAmB;AAC9D,oBAAc;AAAA,IAChB;AACA,UAAM,KAAK,MAAM;AACf,kBAAY,UAAU;AACtB,WAAK,EAAE,KAAK,YAAY,OAAO,EAAE,MAAM,YAAY,MAAM;AACzD,aAAO,YAAY;AAAA,IACrB,CAAC;AACD,QAAI,CAAC;AAAS,gBAAU,MAAM;AAC9B,WAAO,YAAY;AAAA,EACrB;AACA,QAAM,QAAQ,YAAY;AACxB,WAAO,MAAM,QAAQ;AACnB,YAAM,OAAO,MAAM,MAAM;AACzB,YAAM,KAAK,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AAAA,IAC7B;AACA,cAAU;AAAA,EACZ;AACA,SAAO;AAAA,IACL;AAAA,IACA,OAAO,MAAM,WAAW,QAAQ,QAAQ;AAAA,IACxC,IAAI,OAAO;AACT,aAAO,MAAM;AAAA,IACf;AAAA,EACF;AACF;AAEO,IAAM,oBAAoB,CAAW,OAAO,EAAE,kBAAkB,MAAM,MAAM;AACjF,QAAM,SAA+C,CAAC;AACtD,QAAM,OAAO,CAAC,SAAiB,SAAsB;AACnD,QAAI,CAAC,OAAO,OAAO;AAAG,aAAO,OAAO,IAAI,iBAAoB,IAAI;AAChE,WAAO,OAAO,OAAO,EAAE,KAAK,IAAI;AAAA,EAClC;AACA,QAAM,QAAQ,CAAC,YAAoB;AACjC,QAAI,CAAC,OAAO,OAAO;AAAG,aAAO,OAAO,IAAI,iBAAoB,IAAI;AAChE,WAAO,OAAO,OAAO,EAAE,MAAM;AAAA,EAC/B;AACA,SAAO,EAAE,MAAM,MAAM;AACvB;AAEA,IAAM,kBAAN,MAAyC;AAAA,EAMvC,cAAc;AALd,mBAAU;AACV,mBAA2C,MAAM;AAAA,IAAC;AAClD,kBAAyB,MAAM;AAAA,IAAC;AAI9B,SAAK,UAAU,IAAI,QAAW,CAAC,KAAK,QAAQ;AAC1C,WAAK,UAAU;AACf,WAAK,SAAS;AAAA,IAChB,CAAC;AAAA,EACH;AACF;","names":[]} \ No newline at end of file diff --git a/dist/common/SyncLocker.cjs b/dist/common/SyncLocker.cjs new file mode 100644 index 0000000..5c05067 --- /dev/null +++ b/dist/common/SyncLocker.cjs @@ -0,0 +1,94 @@ +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var __decorateClass = (decorators, target, key, kind) => { + var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target; + for (var i = decorators.length - 1, decorator; i >= 0; i--) + if (decorator = decorators[i]) + result = (kind ? decorator(target, key, result) : decorator(result)) || result; + if (kind && result) + __defProp(target, key, result); + return result; +}; + +// src/common/SyncLocker.ts +var SyncLocker_exports = {}; +__export(SyncLocker_exports, { + SyncLocker: () => SyncLocker +}); +module.exports = __toCommonJS(SyncLocker_exports); + +// src/decorators/singleton.ts +var SINGLETON_KEY = Symbol(); +var singleton = (classTarget) => new Proxy(classTarget, { + construct(target, argumentsList, newTarget) { + if (target.prototype !== newTarget.prototype) { + return Reflect.construct(target, argumentsList, newTarget); + } + if (!target[SINGLETON_KEY]) { + target[SINGLETON_KEY] = Reflect.construct(target, argumentsList, newTarget); + } + return target[SINGLETON_KEY]; + } +}); + +// src/common/ZError.ts +var ZError = class { + constructor(statusCode, message) { + this.statusCode = statusCode; + this.message = message; + } +}; + +// src/common/SyncLocker.ts +var SyncLocker = class { + constructor() { + this.map = /* @__PURE__ */ new Map(); + } + lock(req) { + const key = `${req.method}:${req.url}:${req.user?.id || ""}`; + if (this.map.has(key)) { + return false; + } + this.map.set(key, true); + return true; + } + unlock(req) { + const key = `${req.method}:${req.url}:${req.user?.id || ""}`; + this.map.delete(key); + } + checkLock(req) { + const key = `${req.method}:${req.url}:${req.user?.id || ""}`; + if (this.map.has(key)) { + throw new ZError(100, "request too fast"); + } + this.lock(req); + return true; + } + isLocked(req) { + const key = `${req.method}:${req.url}:${req.user?.id || ""}`; + return this.map.has(key); + } +}; +SyncLocker = __decorateClass([ + singleton +], SyncLocker); +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + SyncLocker +}); +//# sourceMappingURL=SyncLocker.cjs.map \ No newline at end of file diff --git a/dist/common/SyncLocker.cjs.map b/dist/common/SyncLocker.cjs.map new file mode 100644 index 0000000..fbcdf47 --- /dev/null +++ b/dist/common/SyncLocker.cjs.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../src/common/SyncLocker.ts","../../src/decorators/singleton.ts","../../src/common/ZError.ts"],"sourcesContent":["import { singleton } from 'decorators/singleton'\nimport { ZError } from './ZError'\n\ninterface IRequest {\n method: string\n url: string\n user?: {\n id: string\n }\n}\n\n@singleton\nexport class SyncLocker {\n map: Map = new Map()\n\n public lock(req: IRequest) {\n const key = `${req.method}:${req.url}:${req.user?.id || ''}`\n if (this.map.has(key)) {\n return false\n }\n this.map.set(key, true)\n return true\n }\n\n public unlock(req: IRequest) {\n const key = `${req.method}:${req.url}:${req.user?.id || ''}`\n this.map.delete(key)\n }\n\n public checkLock(req: IRequest) {\n const key = `${req.method}:${req.url}:${req.user?.id || ''}`\n if (this.map.has(key)) {\n throw new ZError(100, 'request too fast')\n }\n this.lock(req)\n return true\n }\n\n public isLocked(req: IRequest) {\n const key = `${req.method}:${req.url}:${req.user?.id || ''}`\n return this.map.has(key)\n }\n}\n","/**\n * 单例化一个class\n * 使用方法:\n * @singleton\n * class Test {}\n * new Test() === new Test() // returns `true`\n * 也可以不使用 decorator\n * const TestSingleton = singleton(Test)\n * new TestSingleton() === new TestSingleton() //returns 'true'\n */\n\nexport const SINGLETON_KEY = Symbol()\n\nexport type Singleton any> = T & {\n [SINGLETON_KEY]: T extends new (...args: any[]) => infer I ? I : never\n}\nexport const singleton = any>(classTarget: T) =>\n new Proxy(classTarget, {\n construct(target: Singleton, argumentsList, newTarget) {\n // Skip proxy for children\n if (target.prototype !== newTarget.prototype) {\n return Reflect.construct(target, argumentsList, newTarget)\n }\n if (!target[SINGLETON_KEY]) {\n target[SINGLETON_KEY] = Reflect.construct(target, argumentsList, newTarget)\n }\n return target[SINGLETON_KEY]\n },\n })\n","\nexport class ZError implements Error {\n code: string\n statusCode?: number\n message: string\n name: string\n\n constructor(statusCode: number, message: string) {\n this.statusCode = statusCode\n this.message = message\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACWO,IAAM,gBAAgB,OAAO;AAK7B,IAAM,YAAY,CAAwC,gBAC/D,IAAI,MAAM,aAAa;AAAA,EACrB,UAAU,QAAsB,eAAe,WAAW;AAExD,QAAI,OAAO,cAAc,UAAU,WAAW;AAC5C,aAAO,QAAQ,UAAU,QAAQ,eAAe,SAAS;AAAA,IAC3D;AACA,QAAI,CAAC,OAAO,aAAa,GAAG;AAC1B,aAAO,aAAa,IAAI,QAAQ,UAAU,QAAQ,eAAe,SAAS;AAAA,IAC5E;AACA,WAAO,OAAO,aAAa;AAAA,EAC7B;AACF,CAAC;;;AC3BI,IAAM,SAAN,MAA8B;AAAA,EAMnC,YAAY,YAAoB,SAAiB;AAC/C,SAAK,aAAa;AAClB,SAAK,UAAU;AAAA,EACjB;AACF;;;AFCO,IAAM,aAAN,MAAiB;AAAA,EAAjB;AACL,eAA4B,oBAAI,IAAI;AAAA;AAAA,EAE7B,KAAK,KAAe;AACzB,UAAM,MAAM,GAAG,IAAI,MAAM,IAAI,IAAI,GAAG,IAAI,IAAI,MAAM,MAAM,EAAE;AAC1D,QAAI,KAAK,IAAI,IAAI,GAAG,GAAG;AACrB,aAAO;AAAA,IACT;AACA,SAAK,IAAI,IAAI,KAAK,IAAI;AACtB,WAAO;AAAA,EACT;AAAA,EAEO,OAAO,KAAe;AAC3B,UAAM,MAAM,GAAG,IAAI,MAAM,IAAI,IAAI,GAAG,IAAI,IAAI,MAAM,MAAM,EAAE;AAC1D,SAAK,IAAI,OAAO,GAAG;AAAA,EACrB;AAAA,EAEO,UAAU,KAAe;AAC9B,UAAM,MAAM,GAAG,IAAI,MAAM,IAAI,IAAI,GAAG,IAAI,IAAI,MAAM,MAAM,EAAE;AAC1D,QAAI,KAAK,IAAI,IAAI,GAAG,GAAG;AACrB,YAAM,IAAI,OAAO,KAAK,kBAAkB;AAAA,IAC1C;AACA,SAAK,KAAK,GAAG;AACb,WAAO;AAAA,EACT;AAAA,EAEO,SAAS,KAAe;AAC7B,UAAM,MAAM,GAAG,IAAI,MAAM,IAAI,IAAI,GAAG,IAAI,IAAI,MAAM,MAAM,EAAE;AAC1D,WAAO,KAAK,IAAI,IAAI,GAAG;AAAA,EACzB;AACF;AA9Ba,aAAN;AAAA,EADN;AAAA,GACY;","names":[]} \ No newline at end of file diff --git a/dist/common/SyncLocker.d.cts b/dist/common/SyncLocker.d.cts new file mode 100644 index 0000000..8c4931b --- /dev/null +++ b/dist/common/SyncLocker.d.cts @@ -0,0 +1,16 @@ +interface IRequest { + method: string; + url: string; + user?: { + id: string; + }; +} +declare class SyncLocker { + map: Map; + lock(req: IRequest): boolean; + unlock(req: IRequest): void; + checkLock(req: IRequest): boolean; + isLocked(req: IRequest): boolean; +} + +export { SyncLocker }; diff --git a/dist/common/SyncLocker.d.ts b/dist/common/SyncLocker.d.ts new file mode 100644 index 0000000..8c4931b --- /dev/null +++ b/dist/common/SyncLocker.d.ts @@ -0,0 +1,16 @@ +interface IRequest { + method: string; + url: string; + user?: { + id: string; + }; +} +declare class SyncLocker { + map: Map; + lock(req: IRequest): boolean; + unlock(req: IRequest): void; + checkLock(req: IRequest): boolean; + isLocked(req: IRequest): boolean; +} + +export { SyncLocker }; diff --git a/dist/common/SyncLocker.js b/dist/common/SyncLocker.js new file mode 100644 index 0000000..7c0e55f --- /dev/null +++ b/dist/common/SyncLocker.js @@ -0,0 +1,71 @@ +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __decorateClass = (decorators, target, key, kind) => { + var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target; + for (var i = decorators.length - 1, decorator; i >= 0; i--) + if (decorator = decorators[i]) + result = (kind ? decorator(target, key, result) : decorator(result)) || result; + if (kind && result) + __defProp(target, key, result); + return result; +}; + +// src/decorators/singleton.ts +var SINGLETON_KEY = Symbol(); +var singleton = (classTarget) => new Proxy(classTarget, { + construct(target, argumentsList, newTarget) { + if (target.prototype !== newTarget.prototype) { + return Reflect.construct(target, argumentsList, newTarget); + } + if (!target[SINGLETON_KEY]) { + target[SINGLETON_KEY] = Reflect.construct(target, argumentsList, newTarget); + } + return target[SINGLETON_KEY]; + } +}); + +// src/common/ZError.ts +var ZError = class { + constructor(statusCode, message) { + this.statusCode = statusCode; + this.message = message; + } +}; + +// src/common/SyncLocker.ts +var SyncLocker = class { + constructor() { + this.map = /* @__PURE__ */ new Map(); + } + lock(req) { + const key = `${req.method}:${req.url}:${req.user?.id || ""}`; + if (this.map.has(key)) { + return false; + } + this.map.set(key, true); + return true; + } + unlock(req) { + const key = `${req.method}:${req.url}:${req.user?.id || ""}`; + this.map.delete(key); + } + checkLock(req) { + const key = `${req.method}:${req.url}:${req.user?.id || ""}`; + if (this.map.has(key)) { + throw new ZError(100, "request too fast"); + } + this.lock(req); + return true; + } + isLocked(req) { + const key = `${req.method}:${req.url}:${req.user?.id || ""}`; + return this.map.has(key); + } +}; +SyncLocker = __decorateClass([ + singleton +], SyncLocker); +export { + SyncLocker +}; +//# sourceMappingURL=SyncLocker.js.map \ No newline at end of file diff --git a/dist/common/SyncLocker.js.map b/dist/common/SyncLocker.js.map new file mode 100644 index 0000000..d2ba8c9 --- /dev/null +++ b/dist/common/SyncLocker.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../src/decorators/singleton.ts","../../src/common/ZError.ts","../../src/common/SyncLocker.ts"],"sourcesContent":["/**\n * 单例化一个class\n * 使用方法:\n * @singleton\n * class Test {}\n * new Test() === new Test() // returns `true`\n * 也可以不使用 decorator\n * const TestSingleton = singleton(Test)\n * new TestSingleton() === new TestSingleton() //returns 'true'\n */\n\nexport const SINGLETON_KEY = Symbol()\n\nexport type Singleton any> = T & {\n [SINGLETON_KEY]: T extends new (...args: any[]) => infer I ? I : never\n}\nexport const singleton = any>(classTarget: T) =>\n new Proxy(classTarget, {\n construct(target: Singleton, argumentsList, newTarget) {\n // Skip proxy for children\n if (target.prototype !== newTarget.prototype) {\n return Reflect.construct(target, argumentsList, newTarget)\n }\n if (!target[SINGLETON_KEY]) {\n target[SINGLETON_KEY] = Reflect.construct(target, argumentsList, newTarget)\n }\n return target[SINGLETON_KEY]\n },\n })\n","\nexport class ZError implements Error {\n code: string\n statusCode?: number\n message: string\n name: string\n\n constructor(statusCode: number, message: string) {\n this.statusCode = statusCode\n this.message = message\n }\n}\n","import { singleton } from 'decorators/singleton'\nimport { ZError } from './ZError'\n\ninterface IRequest {\n method: string\n url: string\n user?: {\n id: string\n }\n}\n\n@singleton\nexport class SyncLocker {\n map: Map = new Map()\n\n public lock(req: IRequest) {\n const key = `${req.method}:${req.url}:${req.user?.id || ''}`\n if (this.map.has(key)) {\n return false\n }\n this.map.set(key, true)\n return true\n }\n\n public unlock(req: IRequest) {\n const key = `${req.method}:${req.url}:${req.user?.id || ''}`\n this.map.delete(key)\n }\n\n public checkLock(req: IRequest) {\n const key = `${req.method}:${req.url}:${req.user?.id || ''}`\n if (this.map.has(key)) {\n throw new ZError(100, 'request too fast')\n }\n this.lock(req)\n return true\n }\n\n public isLocked(req: IRequest) {\n const key = `${req.method}:${req.url}:${req.user?.id || ''}`\n return this.map.has(key)\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAWO,IAAM,gBAAgB,OAAO;AAK7B,IAAM,YAAY,CAAwC,gBAC/D,IAAI,MAAM,aAAa;AAAA,EACrB,UAAU,QAAsB,eAAe,WAAW;AAExD,QAAI,OAAO,cAAc,UAAU,WAAW;AAC5C,aAAO,QAAQ,UAAU,QAAQ,eAAe,SAAS;AAAA,IAC3D;AACA,QAAI,CAAC,OAAO,aAAa,GAAG;AAC1B,aAAO,aAAa,IAAI,QAAQ,UAAU,QAAQ,eAAe,SAAS;AAAA,IAC5E;AACA,WAAO,OAAO,aAAa;AAAA,EAC7B;AACF,CAAC;;;AC3BI,IAAM,SAAN,MAA8B;AAAA,EAMnC,YAAY,YAAoB,SAAiB;AAC/C,SAAK,aAAa;AAClB,SAAK,UAAU;AAAA,EACjB;AACF;;;ACCO,IAAM,aAAN,MAAiB;AAAA,EAAjB;AACL,eAA4B,oBAAI,IAAI;AAAA;AAAA,EAE7B,KAAK,KAAe;AACzB,UAAM,MAAM,GAAG,IAAI,MAAM,IAAI,IAAI,GAAG,IAAI,IAAI,MAAM,MAAM,EAAE;AAC1D,QAAI,KAAK,IAAI,IAAI,GAAG,GAAG;AACrB,aAAO;AAAA,IACT;AACA,SAAK,IAAI,IAAI,KAAK,IAAI;AACtB,WAAO;AAAA,EACT;AAAA,EAEO,OAAO,KAAe;AAC3B,UAAM,MAAM,GAAG,IAAI,MAAM,IAAI,IAAI,GAAG,IAAI,IAAI,MAAM,MAAM,EAAE;AAC1D,SAAK,IAAI,OAAO,GAAG;AAAA,EACrB;AAAA,EAEO,UAAU,KAAe;AAC9B,UAAM,MAAM,GAAG,IAAI,MAAM,IAAI,IAAI,GAAG,IAAI,IAAI,MAAM,MAAM,EAAE;AAC1D,QAAI,KAAK,IAAI,IAAI,GAAG,GAAG;AACrB,YAAM,IAAI,OAAO,KAAK,kBAAkB;AAAA,IAC1C;AACA,SAAK,KAAK,GAAG;AACb,WAAO;AAAA,EACT;AAAA,EAEO,SAAS,KAAe;AAC7B,UAAM,MAAM,GAAG,IAAI,MAAM,IAAI,IAAI,GAAG,IAAI,IAAI,MAAM,MAAM,EAAE;AAC1D,WAAO,KAAK,IAAI,IAAI,GAAG;AAAA,EACzB;AACF;AA9Ba,aAAN;AAAA,EADN;AAAA,GACY;","names":[]} \ No newline at end of file diff --git a/dist/index.cjs b/dist/index.cjs index 403de24..c23ac1d 100644 --- a/dist/index.cjs +++ b/dist/index.cjs @@ -15,12 +15,24 @@ var __copyProps = (to, from, except, desc) => { return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var __decorateClass = (decorators, target, key, kind) => { + var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target; + for (var i = decorators.length - 1, decorator; i >= 0; i--) + if (decorator = decorators[i]) + result = (kind ? decorator(target, key, result) : decorator(result)) || result; + if (kind && result) + __defProp(target, key, result); + return result; +}; // src/index.ts var src_exports = {}; __export(src_exports, { SINGLETON_KEY: () => SINGLETON_KEY, + SyncLocker: () => SyncLocker, ZError: () => ZError, + createAsyncQueue: () => createAsyncQueue, + createAsyncQueues: () => createAsyncQueues, singleton: () => singleton }); module.exports = __toCommonJS(src_exports); @@ -46,10 +58,114 @@ var singleton = (classTarget) => new Proxy(classTarget, { return target[SINGLETON_KEY]; } }); + +// src/common/SyncLocker.ts +var SyncLocker = class { + constructor() { + this.map = /* @__PURE__ */ new Map(); + } + lock(req) { + const key = `${req.method}:${req.url}:${req.user?.id || ""}`; + if (this.map.has(key)) { + return false; + } + this.map.set(key, true); + return true; + } + unlock(req) { + const key = `${req.method}:${req.url}:${req.user?.id || ""}`; + this.map.delete(key); + } + checkLock(req) { + const key = `${req.method}:${req.url}:${req.user?.id || ""}`; + if (this.map.has(key)) { + throw new ZError(100, "request too fast"); + } + this.lock(req); + return true; + } + isLocked(req) { + const key = `${req.method}:${req.url}:${req.user?.id || ""}`; + return this.map.has(key); + } +}; +SyncLocker = __decorateClass([ + singleton +], SyncLocker); + +// src/common/AsyncQueue.ts +function createAsyncQueue(opts = { dedupeConcurrent: false }) { + const { dedupeConcurrent } = opts; + let queue = []; + let running; + let nextPromise = new DeferredPromise(); + const push = (task) => { + let taskPromise = new DeferredPromise(); + if (dedupeConcurrent) { + queue = []; + if (nextPromise.started) + nextPromise = new DeferredPromise(); + taskPromise = nextPromise; + } + queue.push(() => { + taskPromise.started = true; + task().then(taskPromise.resolve).catch(taskPromise.reject); + return taskPromise.promise; + }); + if (!running) + running = start(); + return taskPromise.promise; + }; + const start = async () => { + while (queue.length) { + const task = queue.shift(); + await task().catch(() => { + }); + } + running = void 0; + }; + return { + push, + flush: () => running || Promise.resolve(), + get size() { + return queue.length; + } + }; +} +var createAsyncQueues = (opts = { dedupeConcurrent: false }) => { + const queues = {}; + const push = (queueId, task) => { + if (!queues[queueId]) + queues[queueId] = createAsyncQueue(opts); + return queues[queueId].push(task); + }; + const flush = (queueId) => { + if (!queues[queueId]) + queues[queueId] = createAsyncQueue(opts); + return queues[queueId].flush(); + }; + return { push, flush }; +}; +var DeferredPromise = class { + constructor() { + this.started = false; + this.resolve = () => { + }; + this.reject = () => { + }; + this.promise = new Promise((res, rej) => { + this.resolve = res; + this.reject = rej; + }); + } +}; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { SINGLETON_KEY, + SyncLocker, ZError, + createAsyncQueue, + createAsyncQueues, singleton }); //# sourceMappingURL=index.cjs.map \ No newline at end of file diff --git a/dist/index.cjs.map b/dist/index.cjs.map index ff90228..4ec5403 100644 --- a/dist/index.cjs.map +++ b/dist/index.cjs.map @@ -1 +1 @@ -{"version":3,"sources":["../src/index.ts","../src/common/ZError.ts","../src/decorators/singleton.ts"],"sourcesContent":["export { ZError } from './common/ZError'\nexport * from './decorators/singleton'","\nexport class ZError implements Error {\n code: string\n statusCode?: number\n message: string\n name: string\n\n constructor(statusCode: number, message: string) {\n this.statusCode = statusCode\n this.message = message\n }\n}\n","/**\n * 单例化一个class\n * 使用方法:\n * @singleton\n * class Test {}\n * new Test() === new Test() // returns `true`\n * 也可以不使用 decorator\n * const TestSingleton = singleton(Test)\n * new TestSingleton() === new TestSingleton() //returns 'true'\n */\n\nexport const SINGLETON_KEY = Symbol()\n\nexport type Singleton any> = T & {\n [SINGLETON_KEY]: T extends new (...args: any[]) => infer I ? I : never\n}\nexport const singleton = any>(classTarget: T) =>\n new Proxy(classTarget, {\n construct(target: Singleton, argumentsList, newTarget) {\n // Skip proxy for children\n if (target.prototype !== newTarget.prototype) {\n return Reflect.construct(target, argumentsList, newTarget)\n }\n if (!target[SINGLETON_KEY]) {\n target[SINGLETON_KEY] = Reflect.construct(target, argumentsList, newTarget)\n }\n return target[SINGLETON_KEY]\n },\n })\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACCO,IAAM,SAAN,MAA8B;AAAA,EAMnC,YAAY,YAAoB,SAAiB;AAC/C,SAAK,aAAa;AAClB,SAAK,UAAU;AAAA,EACjB;AACF;;;ACAO,IAAM,gBAAgB,OAAO;AAK7B,IAAM,YAAY,CAAwC,gBAC/D,IAAI,MAAM,aAAa;AAAA,EACrB,UAAU,QAAsB,eAAe,WAAW;AAExD,QAAI,OAAO,cAAc,UAAU,WAAW;AAC5C,aAAO,QAAQ,UAAU,QAAQ,eAAe,SAAS;AAAA,IAC3D;AACA,QAAI,CAAC,OAAO,aAAa,GAAG;AAC1B,aAAO,aAAa,IAAI,QAAQ,UAAU,QAAQ,eAAe,SAAS;AAAA,IAC5E;AACA,WAAO,OAAO,aAAa;AAAA,EAC7B;AACF,CAAC;","names":[]} \ No newline at end of file +{"version":3,"sources":["../src/index.ts","../src/common/ZError.ts","../src/decorators/singleton.ts","../src/common/SyncLocker.ts","../src/common/AsyncQueue.ts"],"sourcesContent":["export { ZError } from './common/ZError'\nexport { SyncLocker } from './common/SyncLocker'\nexport * from './decorators/singleton'\nexport * from './common/AsyncQueue'","\nexport class ZError implements Error {\n code: string\n statusCode?: number\n message: string\n name: string\n\n constructor(statusCode: number, message: string) {\n this.statusCode = statusCode\n this.message = message\n }\n}\n","/**\n * 单例化一个class\n * 使用方法:\n * @singleton\n * class Test {}\n * new Test() === new Test() // returns `true`\n * 也可以不使用 decorator\n * const TestSingleton = singleton(Test)\n * new TestSingleton() === new TestSingleton() //returns 'true'\n */\n\nexport const SINGLETON_KEY = Symbol()\n\nexport type Singleton any> = T & {\n [SINGLETON_KEY]: T extends new (...args: any[]) => infer I ? I : never\n}\nexport const singleton = any>(classTarget: T) =>\n new Proxy(classTarget, {\n construct(target: Singleton, argumentsList, newTarget) {\n // Skip proxy for children\n if (target.prototype !== newTarget.prototype) {\n return Reflect.construct(target, argumentsList, newTarget)\n }\n if (!target[SINGLETON_KEY]) {\n target[SINGLETON_KEY] = Reflect.construct(target, argumentsList, newTarget)\n }\n return target[SINGLETON_KEY]\n },\n })\n","import { singleton } from 'decorators/singleton'\nimport { ZError } from './ZError'\n\ninterface IRequest {\n method: string\n url: string\n user?: {\n id: string\n }\n}\n\n@singleton\nexport class SyncLocker {\n map: Map = new Map()\n\n public lock(req: IRequest) {\n const key = `${req.method}:${req.url}:${req.user?.id || ''}`\n if (this.map.has(key)) {\n return false\n }\n this.map.set(key, true)\n return true\n }\n\n public unlock(req: IRequest) {\n const key = `${req.method}:${req.url}:${req.user?.id || ''}`\n this.map.delete(key)\n }\n\n public checkLock(req: IRequest) {\n const key = `${req.method}:${req.url}:${req.user?.id || ''}`\n if (this.map.has(key)) {\n throw new ZError(100, 'request too fast')\n }\n this.lock(req)\n return true\n }\n\n public isLocked(req: IRequest) {\n const key = `${req.method}:${req.url}:${req.user?.id || ''}`\n return this.map.has(key)\n }\n}\n","type Callback = () => Promise\n\nexport type AsyncQueue = {\n push: (task: Callback) => Promise\n flush: () => Promise\n size: number\n}\n\n/**\n * Ensures that each callback pushed onto the queue is executed in series.\n * Such a quetie 😻\n * @param opts.dedupeConcurrent If dedupeConcurrent is `true` it ensures that if multiple\n * tasks are pushed onto the queue while there is an active task, only the\n * last one will be executed, once the active task has completed.\n * e.g. in the below example, only 0 and 3 will be executed.\n * ```\n * const queue = createAsyncQueue({ dedupeConcurrent: true })\n * queue.push(async () => console.log(0)) // returns 0\n * queue.push(async () => console.log(1)) // returns 3\n * queue.push(async () => console.log(2)) // returns 3\n * queue.push(async () => console.log(3)) // returns 3\n * ```\n * */\nexport function createAsyncQueue(opts = { dedupeConcurrent: false }): AsyncQueue {\n const { dedupeConcurrent } = opts\n let queue: Callback[] = []\n let running: Promise | undefined\n let nextPromise = new DeferredPromise()\n const push = (task: Callback) => {\n let taskPromise = new DeferredPromise()\n if (dedupeConcurrent) {\n queue = []\n if (nextPromise.started) nextPromise = new DeferredPromise()\n taskPromise = nextPromise\n }\n queue.push(() => {\n taskPromise.started = true\n task().then(taskPromise.resolve).catch(taskPromise.reject)\n return taskPromise.promise\n })\n if (!running) running = start()\n return taskPromise.promise\n }\n const start = async () => {\n while (queue.length) {\n const task = queue.shift()!\n await task().catch(() => {})\n }\n running = undefined\n }\n return {\n push,\n flush: () => running || Promise.resolve(),\n get size() {\n return queue.length\n },\n }\n}\n\nexport const createAsyncQueues = (opts = { dedupeConcurrent: false }) => {\n const queues: { [queueId: string]: AsyncQueue } = {}\n const push = (queueId: string, task: Callback) => {\n if (!queues[queueId]) queues[queueId] = createAsyncQueue(opts)\n return queues[queueId].push(task)\n }\n const flush = (queueId: string) => {\n if (!queues[queueId]) queues[queueId] = createAsyncQueue(opts)\n return queues[queueId].flush()\n }\n return { push, flush }\n}\n\nclass DeferredPromise {\n started = false\n resolve: (x: T | PromiseLike) => void = () => {}\n reject: (x: E) => void = () => {}\n promise: Promise\n\n constructor() {\n this.promise = new Promise((res, rej) => {\n this.resolve = res\n this.reject = rej\n })\n }\n}\n\n// function main() {\n// const queue = createAsyncQueue()\n// queue.push(async () => {\n// console.log(0)\n// }) // returns 0\n// queue.push(async () => {\n// console.log(1)\n\n// return new Promise((resolve, reject) => {\n// setTimeout(() => {\n// console.log('12')\n// resolve()\n// }, 1000)\n// })\n// }) // returns 3\n// queue.push(async () => console.log(2)) // returns 3\n// queue.push(async () => console.log(3)) // returns 3\n// console.log('hi')\n// }\n\n// main()\n"],"mappings":"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","names":[]} \ No newline at end of file diff --git a/dist/index.d.cts b/dist/index.d.cts index 0294735..70443fe 100644 --- a/dist/index.d.cts +++ b/dist/index.d.cts @@ -1,4 +1,6 @@ export { ZError } from './common/ZError.cjs'; +export { SyncLocker } from './common/SyncLocker.cjs'; +export { AsyncQueue, createAsyncQueue, createAsyncQueues } from './common/AsyncQueue.cjs'; /** * 单例化一个class diff --git a/dist/index.d.ts b/dist/index.d.ts index c2db20c..06431ee 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -1,4 +1,6 @@ export { ZError } from './common/ZError.js'; +export { SyncLocker } from './common/SyncLocker.js'; +export { AsyncQueue, createAsyncQueue, createAsyncQueues } from './common/AsyncQueue.js'; /** * 单例化一个class diff --git a/dist/index.js b/dist/index.js index a5632d3..c94713e 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,3 +1,15 @@ +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __decorateClass = (decorators, target, key, kind) => { + var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target; + for (var i = decorators.length - 1, decorator; i >= 0; i--) + if (decorator = decorators[i]) + result = (kind ? decorator(target, key, result) : decorator(result)) || result; + if (kind && result) + __defProp(target, key, result); + return result; +}; + // src/common/ZError.ts var ZError = class { constructor(statusCode, message) { @@ -19,9 +31,113 @@ var singleton = (classTarget) => new Proxy(classTarget, { return target[SINGLETON_KEY]; } }); + +// src/common/SyncLocker.ts +var SyncLocker = class { + constructor() { + this.map = /* @__PURE__ */ new Map(); + } + lock(req) { + const key = `${req.method}:${req.url}:${req.user?.id || ""}`; + if (this.map.has(key)) { + return false; + } + this.map.set(key, true); + return true; + } + unlock(req) { + const key = `${req.method}:${req.url}:${req.user?.id || ""}`; + this.map.delete(key); + } + checkLock(req) { + const key = `${req.method}:${req.url}:${req.user?.id || ""}`; + if (this.map.has(key)) { + throw new ZError(100, "request too fast"); + } + this.lock(req); + return true; + } + isLocked(req) { + const key = `${req.method}:${req.url}:${req.user?.id || ""}`; + return this.map.has(key); + } +}; +SyncLocker = __decorateClass([ + singleton +], SyncLocker); + +// src/common/AsyncQueue.ts +function createAsyncQueue(opts = { dedupeConcurrent: false }) { + const { dedupeConcurrent } = opts; + let queue = []; + let running; + let nextPromise = new DeferredPromise(); + const push = (task) => { + let taskPromise = new DeferredPromise(); + if (dedupeConcurrent) { + queue = []; + if (nextPromise.started) + nextPromise = new DeferredPromise(); + taskPromise = nextPromise; + } + queue.push(() => { + taskPromise.started = true; + task().then(taskPromise.resolve).catch(taskPromise.reject); + return taskPromise.promise; + }); + if (!running) + running = start(); + return taskPromise.promise; + }; + const start = async () => { + while (queue.length) { + const task = queue.shift(); + await task().catch(() => { + }); + } + running = void 0; + }; + return { + push, + flush: () => running || Promise.resolve(), + get size() { + return queue.length; + } + }; +} +var createAsyncQueues = (opts = { dedupeConcurrent: false }) => { + const queues = {}; + const push = (queueId, task) => { + if (!queues[queueId]) + queues[queueId] = createAsyncQueue(opts); + return queues[queueId].push(task); + }; + const flush = (queueId) => { + if (!queues[queueId]) + queues[queueId] = createAsyncQueue(opts); + return queues[queueId].flush(); + }; + return { push, flush }; +}; +var DeferredPromise = class { + constructor() { + this.started = false; + this.resolve = () => { + }; + this.reject = () => { + }; + this.promise = new Promise((res, rej) => { + this.resolve = res; + this.reject = rej; + }); + } +}; export { SINGLETON_KEY, + SyncLocker, ZError, + createAsyncQueue, + createAsyncQueues, singleton }; //# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/index.js.map b/dist/index.js.map index 951e9cc..336e2e5 100644 --- a/dist/index.js.map +++ b/dist/index.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/common/ZError.ts","../src/decorators/singleton.ts"],"sourcesContent":["\nexport class ZError implements Error {\n code: string\n statusCode?: number\n message: string\n name: string\n\n constructor(statusCode: number, message: string) {\n this.statusCode = statusCode\n this.message = message\n }\n}\n","/**\n * 单例化一个class\n * 使用方法:\n * @singleton\n * class Test {}\n * new Test() === new Test() // returns `true`\n * 也可以不使用 decorator\n * const TestSingleton = singleton(Test)\n * new TestSingleton() === new TestSingleton() //returns 'true'\n */\n\nexport const SINGLETON_KEY = Symbol()\n\nexport type Singleton any> = T & {\n [SINGLETON_KEY]: T extends new (...args: any[]) => infer I ? I : never\n}\nexport const singleton = any>(classTarget: T) =>\n new Proxy(classTarget, {\n construct(target: Singleton, argumentsList, newTarget) {\n // Skip proxy for children\n if (target.prototype !== newTarget.prototype) {\n return Reflect.construct(target, argumentsList, newTarget)\n }\n if (!target[SINGLETON_KEY]) {\n target[SINGLETON_KEY] = Reflect.construct(target, argumentsList, newTarget)\n }\n return target[SINGLETON_KEY]\n },\n })\n"],"mappings":";AACO,IAAM,SAAN,MAA8B;AAAA,EAMnC,YAAY,YAAoB,SAAiB;AAC/C,SAAK,aAAa;AAClB,SAAK,UAAU;AAAA,EACjB;AACF;;;ACAO,IAAM,gBAAgB,OAAO;AAK7B,IAAM,YAAY,CAAwC,gBAC/D,IAAI,MAAM,aAAa;AAAA,EACrB,UAAU,QAAsB,eAAe,WAAW;AAExD,QAAI,OAAO,cAAc,UAAU,WAAW;AAC5C,aAAO,QAAQ,UAAU,QAAQ,eAAe,SAAS;AAAA,IAC3D;AACA,QAAI,CAAC,OAAO,aAAa,GAAG;AAC1B,aAAO,aAAa,IAAI,QAAQ,UAAU,QAAQ,eAAe,SAAS;AAAA,IAC5E;AACA,WAAO,OAAO,aAAa;AAAA,EAC7B;AACF,CAAC;","names":[]} \ No newline at end of file +{"version":3,"sources":["../src/common/ZError.ts","../src/decorators/singleton.ts","../src/common/SyncLocker.ts","../src/common/AsyncQueue.ts"],"sourcesContent":["\nexport class ZError implements Error {\n code: string\n statusCode?: number\n message: string\n name: string\n\n constructor(statusCode: number, message: string) {\n this.statusCode = statusCode\n this.message = message\n }\n}\n","/**\n * 单例化一个class\n * 使用方法:\n * @singleton\n * class Test {}\n * new Test() === new Test() // returns `true`\n * 也可以不使用 decorator\n * const TestSingleton = singleton(Test)\n * new TestSingleton() === new TestSingleton() //returns 'true'\n */\n\nexport const SINGLETON_KEY = Symbol()\n\nexport type Singleton any> = T & {\n [SINGLETON_KEY]: T extends new (...args: any[]) => infer I ? I : never\n}\nexport const singleton = any>(classTarget: T) =>\n new Proxy(classTarget, {\n construct(target: Singleton, argumentsList, newTarget) {\n // Skip proxy for children\n if (target.prototype !== newTarget.prototype) {\n return Reflect.construct(target, argumentsList, newTarget)\n }\n if (!target[SINGLETON_KEY]) {\n target[SINGLETON_KEY] = Reflect.construct(target, argumentsList, newTarget)\n }\n return target[SINGLETON_KEY]\n },\n })\n","import { singleton } from 'decorators/singleton'\nimport { ZError } from './ZError'\n\ninterface IRequest {\n method: string\n url: string\n user?: {\n id: string\n }\n}\n\n@singleton\nexport class SyncLocker {\n map: Map = new Map()\n\n public lock(req: IRequest) {\n const key = `${req.method}:${req.url}:${req.user?.id || ''}`\n if (this.map.has(key)) {\n return false\n }\n this.map.set(key, true)\n return true\n }\n\n public unlock(req: IRequest) {\n const key = `${req.method}:${req.url}:${req.user?.id || ''}`\n this.map.delete(key)\n }\n\n public checkLock(req: IRequest) {\n const key = `${req.method}:${req.url}:${req.user?.id || ''}`\n if (this.map.has(key)) {\n throw new ZError(100, 'request too fast')\n }\n this.lock(req)\n return true\n }\n\n public isLocked(req: IRequest) {\n const key = `${req.method}:${req.url}:${req.user?.id || ''}`\n return this.map.has(key)\n }\n}\n","type Callback = () => Promise\n\nexport type AsyncQueue = {\n push: (task: Callback) => Promise\n flush: () => Promise\n size: number\n}\n\n/**\n * Ensures that each callback pushed onto the queue is executed in series.\n * Such a quetie 😻\n * @param opts.dedupeConcurrent If dedupeConcurrent is `true` it ensures that if multiple\n * tasks are pushed onto the queue while there is an active task, only the\n * last one will be executed, once the active task has completed.\n * e.g. in the below example, only 0 and 3 will be executed.\n * ```\n * const queue = createAsyncQueue({ dedupeConcurrent: true })\n * queue.push(async () => console.log(0)) // returns 0\n * queue.push(async () => console.log(1)) // returns 3\n * queue.push(async () => console.log(2)) // returns 3\n * queue.push(async () => console.log(3)) // returns 3\n * ```\n * */\nexport function createAsyncQueue(opts = { dedupeConcurrent: false }): AsyncQueue {\n const { dedupeConcurrent } = opts\n let queue: Callback[] = []\n let running: Promise | undefined\n let nextPromise = new DeferredPromise()\n const push = (task: Callback) => {\n let taskPromise = new DeferredPromise()\n if (dedupeConcurrent) {\n queue = []\n if (nextPromise.started) nextPromise = new DeferredPromise()\n taskPromise = nextPromise\n }\n queue.push(() => {\n taskPromise.started = true\n task().then(taskPromise.resolve).catch(taskPromise.reject)\n return taskPromise.promise\n })\n if (!running) running = start()\n return taskPromise.promise\n }\n const start = async () => {\n while (queue.length) {\n const task = queue.shift()!\n await task().catch(() => {})\n }\n running = undefined\n }\n return {\n push,\n flush: () => running || Promise.resolve(),\n get size() {\n return queue.length\n },\n }\n}\n\nexport const createAsyncQueues = (opts = { dedupeConcurrent: false }) => {\n const queues: { [queueId: string]: AsyncQueue } = {}\n const push = (queueId: string, task: Callback) => {\n if (!queues[queueId]) queues[queueId] = createAsyncQueue(opts)\n return queues[queueId].push(task)\n }\n const flush = (queueId: string) => {\n if (!queues[queueId]) queues[queueId] = createAsyncQueue(opts)\n return queues[queueId].flush()\n }\n return { push, flush }\n}\n\nclass DeferredPromise {\n started = false\n resolve: (x: T | PromiseLike) => void = () => {}\n reject: (x: E) => void = () => {}\n promise: Promise\n\n constructor() {\n this.promise = new Promise((res, rej) => {\n this.resolve = res\n this.reject = rej\n })\n }\n}\n\n// function main() {\n// const queue = createAsyncQueue()\n// queue.push(async () => {\n// console.log(0)\n// }) // returns 0\n// queue.push(async () => {\n// console.log(1)\n\n// return new Promise((resolve, reject) => {\n// setTimeout(() => {\n// console.log('12')\n// resolve()\n// }, 1000)\n// })\n// }) // returns 3\n// queue.push(async () => console.log(2)) // returns 3\n// queue.push(async () => console.log(3)) // returns 3\n// console.log('hi')\n// }\n\n// main()\n"],"mappings":"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","names":[]} \ No newline at end of file diff --git a/src/common/AsyncQueue.ts b/src/common/AsyncQueue.ts new file mode 100644 index 0000000..792c27e --- /dev/null +++ b/src/common/AsyncQueue.ts @@ -0,0 +1,107 @@ +type Callback = () => Promise + +export type AsyncQueue = { + push: (task: Callback) => Promise + flush: () => Promise + size: number +} + +/** + * Ensures that each callback pushed onto the queue is executed in series. + * Such a quetie 😻 + * @param opts.dedupeConcurrent If dedupeConcurrent is `true` it ensures that if multiple + * tasks are pushed onto the queue while there is an active task, only the + * last one will be executed, once the active task has completed. + * e.g. in the below example, only 0 and 3 will be executed. + * ``` + * const queue = createAsyncQueue({ dedupeConcurrent: true }) + * queue.push(async () => console.log(0)) // returns 0 + * queue.push(async () => console.log(1)) // returns 3 + * queue.push(async () => console.log(2)) // returns 3 + * queue.push(async () => console.log(3)) // returns 3 + * ``` + * */ +export function createAsyncQueue(opts = { dedupeConcurrent: false }): AsyncQueue { + const { dedupeConcurrent } = opts + let queue: Callback[] = [] + let running: Promise | undefined + let nextPromise = new DeferredPromise() + const push = (task: Callback) => { + let taskPromise = new DeferredPromise() + if (dedupeConcurrent) { + queue = [] + if (nextPromise.started) nextPromise = new DeferredPromise() + taskPromise = nextPromise + } + queue.push(() => { + taskPromise.started = true + task().then(taskPromise.resolve).catch(taskPromise.reject) + return taskPromise.promise + }) + if (!running) running = start() + return taskPromise.promise + } + const start = async () => { + while (queue.length) { + const task = queue.shift()! + await task().catch(() => {}) + } + running = undefined + } + return { + push, + flush: () => running || Promise.resolve(), + get size() { + return queue.length + }, + } +} + +export const createAsyncQueues = (opts = { dedupeConcurrent: false }) => { + const queues: { [queueId: string]: AsyncQueue } = {} + const push = (queueId: string, task: Callback) => { + if (!queues[queueId]) queues[queueId] = createAsyncQueue(opts) + return queues[queueId].push(task) + } + const flush = (queueId: string) => { + if (!queues[queueId]) queues[queueId] = createAsyncQueue(opts) + return queues[queueId].flush() + } + return { push, flush } +} + +class DeferredPromise { + started = false + resolve: (x: T | PromiseLike) => void = () => {} + reject: (x: E) => void = () => {} + promise: Promise + + constructor() { + this.promise = new Promise((res, rej) => { + this.resolve = res + this.reject = rej + }) + } +} + +// function main() { +// const queue = createAsyncQueue() +// queue.push(async () => { +// console.log(0) +// }) // returns 0 +// queue.push(async () => { +// console.log(1) + +// return new Promise((resolve, reject) => { +// setTimeout(() => { +// console.log('12') +// resolve() +// }, 1000) +// }) +// }) // returns 3 +// queue.push(async () => console.log(2)) // returns 3 +// queue.push(async () => console.log(3)) // returns 3 +// console.log('hi') +// } + +// main() diff --git a/src/common/SyncLocker.ts b/src/common/SyncLocker.ts new file mode 100644 index 0000000..43ec5b2 --- /dev/null +++ b/src/common/SyncLocker.ts @@ -0,0 +1,43 @@ +import { singleton } from 'decorators/singleton' +import { ZError } from './ZError' + +interface IRequest { + method: string + url: string + user?: { + id: string + } +} + +@singleton +export class SyncLocker { + map: Map = new Map() + + public lock(req: IRequest) { + const key = `${req.method}:${req.url}:${req.user?.id || ''}` + if (this.map.has(key)) { + return false + } + this.map.set(key, true) + return true + } + + public unlock(req: IRequest) { + const key = `${req.method}:${req.url}:${req.user?.id || ''}` + this.map.delete(key) + } + + public checkLock(req: IRequest) { + const key = `${req.method}:${req.url}:${req.user?.id || ''}` + if (this.map.has(key)) { + throw new ZError(100, 'request too fast') + } + this.lock(req) + return true + } + + public isLocked(req: IRequest) { + const key = `${req.method}:${req.url}:${req.user?.id || ''}` + return this.map.has(key) + } +} diff --git a/src/index.ts b/src/index.ts index 368d921..888d996 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,4 @@ export { ZError } from './common/ZError' -export * from './decorators/singleton' \ No newline at end of file +export { SyncLocker } from './common/SyncLocker' +export * from './decorators/singleton' +export * from './common/AsyncQueue' \ No newline at end of file