diff --git a/10-push-to-npm.bat b/10-push-to-npm.bat index 70826c8..67b14bc 100644 --- a/10-push-to-npm.bat +++ b/10-push-to-npm.bat @@ -4,8 +4,8 @@ cd "%DIR%" node contribute/toBrowser.js (npm publish --registry https://npm.zema.de/) && ( node contribute/toNodejs.js - npm publish --registry https://npm.zema.de/ + npm publish --registry https://npm.zema.de/ --tag latest ) || ( node contribute/toNodejs.js - npm publish --registry https://npm.zema.de/ + npm publish --registry https://npm.zema.de/ --tag latest ) diff --git a/CHANGELOG.md b/CHANGELOG.md index de03721..6a2df28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -216,4 +216,19 @@ Inital commit, which is working with the browser # 1.3.9 - Fixing: - - `helpers/limit`: Now enrows all functions provided. \ No newline at end of file + - `helpers/limit`: Now enrows all functions provided. + +# 1.3.10 + - Modified: + - `helpers/limit`: Adding parameter `assignControlFunction` to assing the controll function. + - `helpers/index`: modified the export of the `limit` stuff. + - Added: + - `helpers/functionMethods*`: Added helpers for functions + +# 1.3.11 + - Modified: + - `helpers/functionMethods`: Adding `asnyc` detection + +# 1.3.12 + - Modified: + - `helpers/limit`: added the option `minDelay`. If provided, the calles are ensured to be delayed with this options. If `0` or smaller -> no delay is added. diff --git a/contribute/VERSION b/contribute/VERSION index 2fe9589..ba50e86 100644 --- a/contribute/VERSION +++ b/contribute/VERSION @@ -1 +1 @@ -1.3.9 \ No newline at end of file +1.3.12 \ No newline at end of file diff --git a/lib/helpers/functionMethods.spec.ts b/lib/helpers/functionMethods.spec.ts new file mode 100644 index 0000000..65cb109 --- /dev/null +++ b/lib/helpers/functionMethods.spec.ts @@ -0,0 +1,278 @@ +import { assert } from "chai"; +import { describe, it } from "mocha"; +import { + countAllArguments, + countArguments, + fillOptionalArguments, + _extractArgumentsPartFromFunction, +} from "./functionMethods"; + +describe("functionMethods", function () { + // Describe the required Test: + describe("countArguments - static functions", function () { + function test1() { + console.log("test"); + } + + function test2(a, b, c) { + console.log("test"); + } + + function test3(a, b, c = "hello") { + console.log("test"); + } + + function test4(a, b, c = "with, comma") { + console.log("test"); + } + + function test5(a, b, c = "with escaped ' quote and, comma") { + console.log("test"); + } + + function test6( + a, + b, + c = "with escaped ' quote and, comma", + d = 'and double " quotes, too!' + ) { + console.log("test"); + } + + function test7( + a, + b, + c = "testFuncCalls".substr(1, 2), + d = "or maybe (string parenthesis)", + e = Math.sqrt(Math.pow(5, 2)) + ) { + console.log("test"); + } + + function test8(betterMakeSure, itWorksWith, longVariables = "too") { + console.log("test"); + } + + function test9(single) { + console.log("test"); + } + + function test10(single = "default") { + console.log("test"); + } + + function test11(a, b, c = { objects: true, test: "," }) { + console.log("test"); + } + + function test12(a, b, arrays = [true, 2, "three, "]) { + console.log("test"); + } + + function test13(a = "endingEmptyParenths".toString()) { + console.log("test"); + } + + function test14(singleInDouble = "'", b = 1) { + console.log("test"); + } + + function test15(doubleInSingle = '"', b = 1) { + console.log("test"); + } + + function test16(doubleInSingle = '"', b = 1, ...args) { + console.log("test"); + } + + function test17(p1: (p1, p2) => {}, p2) { + console.log("test"); + } + + function test18(p1: (p1, p2) => {}, p2 = null) { + console.log("test"); + } + + function test19(p1: (p1, p2) => number = (a, b) => 1, p2 = null) {} + + const tests = [ + [test1, 0, 0], + [test2, 3, 0], + [test3, 2, 1], + [test4, 2, 1], + [test5, 2, 1], + [test6, 2, 2], + [test7, 2, 2], + [test8, 2, 1], + [test9, 1, 0], + [test10, 0, 1], + [test11, 2, 1], + [test12, 2, 1], + [test13, 0, 1], + [test14, 0, 2], + [test15, 0, 2], + [test16, 0, 3], + [test17, 2, 0], + [test18, 1, 1], + [test19, 0, 2], + ]; + let idx = 1; + for (const [func, s, o] of tests) { + it(`correct counting of test${idx++}`, function () { + const res = countArguments(func); + assert.equal( + res.static, + s, + `Expecting static - parameters to ${s} to match ${res.static}` + ); + assert.equal( + res.optional, + o, + `Expecting ${o} to match ${res.optional}` + ); + }); + } + }); + + describe("countArguments - arrow functions", function () { + const tests = [ + [/* 1*/ function empty() {}, 0], + [/* 2*/ function simple(a, b, c) {}, 3], + [/* 3*/ function withString(a, b, c = "hello") {}, 3], + [/* 4*/ function withStringAndComma(a, b, c = "with, comma") {}, 3], + [ + /* 5*/ function withEscapedStuffInStringValue( + a, + b, + c = "with escaped ' quote and, comma" + ) {}, + 3, + ], + [ + /* 6*/ function withEscapedStuffAndCommaInStringValue( + a, + b, + c = "with escaped ' quote and, comma", + d = 'and double " quotes, too!' + ) {}, + 4, + ], + [ + /* 7*/ function withParenthesisInStringValues( + a, + b, + c = "testFuncCalls".slice(1, 2), + d = "or maybe (string parenthesis)", + e = Math.sqrt(Math.pow(5, 2)) + ) {}, + 4, + ], + [ + /* 8*/ function (betterMakeSure, itWorksWith, longVariables = "too") {}, + 3, + ], + [/* 9*/ function (single) {}, 1], + [/*10*/ function (single = "default") {}, 1], + [/*11*/ function (a, b, c = { objects: true, test: "," }) {}, 3], + [/*12*/ function (a, b, arrays = [true, 2, "three, "]) {}, 3], + [/*13*/ function (a = "endingEmptyParenths".toString()) {}, 1], + [/*14*/ function (singleInDouble = "'", b = 1) {}, 2], + [/*15*/ function (doubleInSingle = '"', b = 1) {}, 2], + [/*16*/ () => {}, 0], + [/*17*/ (_ = 23) => {}, 1], + [/*18*/ (a) => {}, 1], + [/*19*/ (...a) => {}, 1], + [ + /*20*/ (a) => { + return a; + }, + 1, + ], + [/*21*/ (b = 1, a = {}) => {}, 2], + [/*22*/ (b = 1, a = [1, 2, 3]) => {}, 2], + // [/*23*/ function (foo, bar) {}.bind(null), 2], // 23 }' + [/*24*/ (a) => a, 1], + [/*25*/ (b, c, a = [1, 2, 4].map((v) => "x,y")) => `hello`, 3], + [/*26*/ (_ = 42) => console.log("test"), 1], + // [ + // /*27*/ (a = `\(42\)`, b = `"blablabla,\",\"foo,\",\"bar"`) => + // console.log("test"), + // 2, + // ], + [ + /*28*/ (a = `([42, 43]\``, b = `"blablabla, foo(, \`\'bar:))"`) => + console.log("test"), + 2, + ], + [ + /*29*/ ( + a = `\([42, 43]\)`, + b = function (a, b, c = "foo, also: bar") { + let x = [a, b]; + return x; + } + ) => console.log("test"), + 2, + ], + [ + /*30*/ ( + a = { + x: 'some" comma\'s, unclosed brackets, }},[, (, and escapes and whatnot"\\', + }, + b + ) => console.log("test"), + 2, + ], + ]; + for (const [func, all] of tests) { + it(`correct counting of: ${_extractArgumentsPartFromFunction( + func + )}`, function () { + const res = countArguments(func); + assert.equal( + res.total, + all, + `Expecting static - parameters to ${all} to match ${res.total}` + ); + }); + } + }); + + describe("auto-fill", function () { + function test(p1, p2, p3 = null, p4 = null) { + return [p1, p2, p3, p4]; + } + + const tests: Array<[any[], any[], boolean, any[]]> = [ + [[0, 1], [], false, [0, 1, undefined, undefined]], + [[0, 1], [2], false, [0, 1, 2, undefined]], + [[0, 1], [3], true, [0, 1, undefined, 3]], + [[0, 1], [2, 3], false, [0, 1, 2, 3]], + [[0, 1], [2, 3], true, [0, 1, 2, 3]], + ]; + + for (const params of tests) { + it(`auto assigning parameters of: ${params.slice(0, 3)}`, function () { + const p = fillOptionalArguments(test, params[0], params[1], params[2]); + const r = params[3]; + assert.deepEqual( + p, + r, + `Expecting static - parameters to ${p} to match ${r}` + ); + + const res = r.map((item) => { + if (item === undefined) { + return null; + } else return item; + }); + + assert.deepEqual( + (test as any)(...p), + res, + `Expecting the result to be equal ${p} to match ${res}` + ); + }); + } + }); +}); diff --git a/lib/helpers/functionMethods.ts b/lib/helpers/functionMethods.ts new file mode 100644 index 0000000..ebf9f1a --- /dev/null +++ b/lib/helpers/functionMethods.ts @@ -0,0 +1,132 @@ +const RE_EXTRACT_ARGS = /(^[a-z_](?=(=>|=>{)))|((^\([^)].+\)|\(\))(?=(=>|{)))/g; +const RE_VALUE_PARAMS = /(?<=[`"'])([^\`,].+?)(?=[`"'])/g; + +/** + * Helper to extrat the code of the function: + * + * @example + * ```javascript + * function func(betterMakeSure, itWorksWith, longVariables = 'too') {} + * + * const r = extractArgumentsPartFromFunction(func); + * + * // => r = (betterMakeSure,itWorksWith,longVariables='too') + * + * ``` + * @param func + * @returns + */ +export function _extractArgumentsPartFromFunction(func): string { + /** + * Based on the given sources: + * + * Source: https://stackoverflow.com/questions/42899083/get-function-parameter-length-including-default-params + * Source: https://stackblitz.com/edit/web-platform-jaxz82?file=script.js + * + * added the async related errors. + */ + + let fnStr = func + .toString() + .replace("async", "") + .replace(RegExp(`\\s|function|${func.name}`, `g`), ``); + fnStr = (fnStr.match(RE_EXTRACT_ARGS) || [fnStr])[0].replace( + RE_VALUE_PARAMS, + `` + ); + return !fnStr.startsWith(`(`) ? `(${fnStr})` : fnStr; +} + +/** + * Helper to count all arguments of an function (including the optional ones.) + * @param func The funtion to chekc + * @returns + */ +export function countAllArguments(func) { + /** + * Source: https://stackoverflow.com/questions/42899083/get-function-parameter-length-including-default-params + * Source: https://stackblitz.com/edit/web-platform-jaxz82?file=script.js + */ + + const params = _extractArgumentsPartFromFunction(func); + + if (params === "()") return 0; + + let [commaCount, bracketCount, bOpen, bClose] = [ + 0, + 0, + [...`([{`], + [...`)]}`], + ]; + [...params].forEach((chr) => { + bracketCount += bOpen.includes(chr) ? 1 : bClose.includes(chr) ? -1 : 0; + commaCount += chr === "," && bracketCount === 1 ? 1 : 0; + }); + return commaCount + 1; +} + +/** + * Helper to count the arguments. + * @param func The function ot be analysed + * @returns + */ +export function countArguments(func): { + optional: number; + static: number; + total: number; +} { + const usedArguments = countAllArguments(func); + return { + optional: usedArguments - func.length, + static: func.length, + total: usedArguments, + }; +} + +/** + * Helper to fill provided arguments for the function. + * @param func The function ot be analysed + * @param providedArg The allready provided args + * @param argsToFill The Arguments to fill + * @param fromEnd A Flag to toggle, whether the arguments should be filled from the end or the beginning. + */ +export function fillOptionalArguments( + func, + providedArg: any[], + argsToFill: any[], + fromEnd = true +) { + const argumentOptions = countArguments(func); + + if (argsToFill.length > argumentOptions.optional) { + // More arguments provided as possible => give a warning + } + + if ( + argumentOptions.optional > 0 && + argumentOptions.total > providedArg.length + ) { + // Fill the arguments + const left = argumentOptions.total - providedArg.length; + for (let i = 0; i < left; i++) { + providedArg.push(undefined); + } + + // Now we cann fill some arguments. + const sourceOffset = + left >= argsToFill.length ? 0 : argsToFill.length - left; + for (let i = 0; i < argsToFill.length; i++) { + let idxToWrite = 0; + + if (fromEnd) { + idxToWrite = argumentOptions.total - argsToFill.length + i; + } else { + idxToWrite = argumentOptions.total - left + i; + } + + providedArg[idxToWrite] = argsToFill[sourceOffset + i]; + } + } + + return providedArg; +} diff --git a/lib/helpers/index.browser.ts b/lib/helpers/index.browser.ts index 9addc93..7a17766 100644 --- a/lib/helpers/index.browser.ts +++ b/lib/helpers/index.browser.ts @@ -6,6 +6,7 @@ import * as arrays from "./arrayMethods"; import * as async from "./async"; import * as descriptors from "./descriptors"; +import * as functions from "./functionMethods"; import * as subject from "./getSubject"; import * as ids from "./idMethods"; import * as json from "./jsonMethods"; @@ -22,6 +23,7 @@ import * as strings from "./stringMethods"; export * from "./arrayMethods"; export * from "./async"; export * from "./descriptors"; +export * from "./functionMethods"; export * from "./getSubject"; export * from "./idMethods"; export * from "./jsonMethods"; @@ -49,5 +51,6 @@ export { runtime, subject, descriptors, - limit as lock, + functions, + limit, }; diff --git a/lib/helpers/limit.spec.ts b/lib/helpers/limit.spec.ts index 4c35f19..dfadf3f 100644 --- a/lib/helpers/limit.spec.ts +++ b/lib/helpers/limit.spec.ts @@ -24,6 +24,34 @@ describe("limit", function () { throw Error("Failed to call sync"); } }); + + it("single-call - with locking", async () => { + const sleepExtended = (delay, opts) => { + return new Promise((resolve) => { + opts.pauseTask(); + setTimeout(() => { + opts.continueTask(); + resolve(null); + }, delay); + }); + }; + + const f = limitedCalls(sleepExtended, { + maxParallel: 0, + assignControlFunction(args, opts) { + args.push(opts); + return args; + }, + }); + const start = Date.now(); + const promises = [f(100), f(100)]; + await Promise.all(promises); + const end = Date.now(); + if (end - start > 150) { + throw Error("Failed to call async"); + } + }); + it("single-call - parallel", async () => { const f = limitedCalls(sleep, { maxParallel: 2, @@ -62,5 +90,31 @@ describe("limit", function () { throw Error("Failed to call callbackBetween"); } }); + it("single-call - delay", async () => { + const f = limitedCalls(async (...args) => {}, { + maxParallel: 0, + minDelay: 50, + }); + const start = Date.now(); + const promises = [f(100), f(100)]; + await Promise.all(promises); + const end = Date.now(); + if (end - start < 50) { + throw Error("Failed to call callbackBetween"); + } + }); + it("single-call - delay (parallel)", async () => { + const f = limitedCalls(async (...args) => {}, { + maxParallel: 10, + minDelay: 50, + }); + const start = Date.now(); + const promises = [f(100), f(100)]; + await Promise.all(promises); + const end = Date.now(); + if (end - start < 50) { + throw Error("Failed to call callbackBetween"); + } + }); }); }); diff --git a/lib/helpers/limit.ts b/lib/helpers/limit.ts index 028b067..1c52c8c 100644 --- a/lib/helpers/limit.ts +++ b/lib/helpers/limit.ts @@ -16,34 +16,42 @@ export type TLimitedOptions = { * The Id to use. If not provided, an specific id is generated */ functionId: string; + /** * An queue that should be used. If not provided, a queue is used. */ queue: Array<[string, string, any[]]>; + /** * Mapping for the Functions. */ mapping: { [index: string]: (...args) => Promise }; + /** * An emitter to use. */ emitter: EventEmitter; + /** * Helper function to request a lock. */ getLock: (functionId: string, newTaskId: string) => boolean; + /** * An additional function, wich can be used between the next function in is called. e.g. sleep. */ callbackBetween?: () => Promise; + /** * Number of elements, which could be called in parallel. 0 = sequntial */ maxParallel: number; + /** * A logger to use. */ loggerLevel: false | LoggerLevel; + /** * An overview with active Tasks. This is relevant for multiple Funtions. */ @@ -53,6 +61,21 @@ export type TLimitedOptions = { * An overview with active Tasks. This is relevant for multiple Funtions. */ awaitingTasks: Set; + + /** + * Helper to assign the control function, for example on an async function. + */ + assignControlFunction: ( + args: any[], + functions: { + pauseTask: () => void; + continueTask: () => void; + } + ) => any[]; + + minDelay: number; + + lastDone: number; }; /** @@ -112,10 +135,15 @@ export function limitedCalls( settingsToUse.maxParallel < 0 || tasks.size <= settingsToUse.maxParallel ); }, + assignControlFunction: (args, opts) => { + return args; + }, maxParallel: 0, loggerLevel: false, activeTasks: new Set(), awaitingTasks: new Set(), + minDelay: -1, + lastDone: Date.now(), }; const settingsToUse: TLimitedOptions = Object.assign(defaultSettins, options); @@ -138,6 +166,7 @@ export function limitedCalls( ); } settingsToUse.awaitingTasks.add(taskId); + settingsToUse.emitter.emit("execute"); }; const continueTask = () => { @@ -147,10 +176,14 @@ export function limitedCalls( ); } settingsToUse.awaitingTasks.delete(taskId); + settingsToUse.emitter.emit("execute"); }; // Add the functions. - args.push(pauseTask, continueTask); + args = settingsToUse.assignControlFunction(args, { + pauseTask, + continueTask, + }); // Push the Content to the emitter settingsToUse.queue.push([functionId, taskId, args]); @@ -189,8 +222,10 @@ export function limitedCalls( ); } + settingsToUse.lastDone = Date.now(); + // Emit, that there is a new task available - settingsToUse.emitter.emit("data"); + settingsToUse.emitter.emit("execute"); }) .catch((_) => { // Log some stuff @@ -200,8 +235,10 @@ export function limitedCalls( ); } + settingsToUse.lastDone = Date.now(); + // Emit, that there is a new task available - settingsToUse.emitter.emit("data", settingsToUse.functionId); + settingsToUse.emitter.emit("execute"); }); } else { if (logger) { @@ -210,8 +247,10 @@ export function limitedCalls( ); } + settingsToUse.lastDone = Date.now(); + // Emit, that there is a new task available - settingsToUse.emitter.emit("data", settingsToUse.functionId); + settingsToUse.emitter.emit("execute"); } }; @@ -222,18 +261,34 @@ export function limitedCalls( reject = _reject; }); - settingsToUse.emitter.emit("data"); + settingsToUse.emitter.emit("execute"); return promise; }; - if (settingsToUse.emitter.listeners("data").length == 0) { - settingsToUse.emitter.on("data", () => { + if (settingsToUse.emitter.listeners("execute").length == 0) { + const tryExecuteTask = () => { if (settingsToUse.queue.length > 0) { // Get the Id and the Args. const [functionId, taskId, args] = settingsToUse.queue[0]; if (settingsToUse.getLock(functionId, taskId)) { + const diff = Date.now() - settingsToUse.lastDone; + + if (settingsToUse.minDelay > 0 && diff < settingsToUse.minDelay) { + // Recall our routine + setTimeout( + tryExecuteTask, + settingsToUse.minDelay - diff + 10, + null + ); + return; + } + + if (settingsToUse.maxParallel > 0) { + settingsToUse.lastDone = Date.now(); + } + // Add the Task as active. settingsToUse.activeTasks.add(taskId); @@ -270,7 +325,9 @@ export function limitedCalls( } } } - }); + }; + + settingsToUse.emitter.on("execute", tryExecuteTask); } return wrapped; diff --git a/package.json b/package.json index e204b6e..a3a3a7c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nope", - "version": "1.3.9", + "version": "1.3.12", "description": "NoPE Runtime for Nodejs. For Browser-Support please use nope-browser", "files": [ "dist-nodejs/**/*",