tiny-essentials 1.5.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,6 +13,7 @@ class TinyPromiseQueue {
13
13
  * @property {(value: any) => any} resolve - The resolve function from the Promise.
14
14
  * @property {(reason?: any) => any} reject - The reject function from the Promise.
15
15
  * @property {string|undefined} [id] - Optional identifier for the task.
16
+ * @property {string|null|undefined} [marker] - Optional marker for the task.
16
17
  * @property {number|null|undefined} [delay] - Optional delay (in ms) before the task is executed.
17
18
  */
18
19
  /** @type {Array<QueuedTask>} */
@@ -31,22 +32,26 @@ class TinyPromiseQueue {
31
32
  return this.#running;
32
33
  }
33
34
  /**
34
- * Processes the next task in the queue if not already running.
35
- * Ensures tasks are executed in order, one at a time.
35
+ * Processes the a normal task.
36
+ *
37
+ * @param {QueuedTask} data
36
38
  *
37
39
  * @returns {Promise<void>}
38
40
  */
39
- async #processQueue() {
40
- if (this.#running || this.#queue.length === 0)
41
- return;
42
- this.#running = true;
43
- const data = this.#queue.shift();
41
+ async #normalProcessQueue(data) {
44
42
  if (data &&
45
43
  typeof data.task === 'function' &&
46
44
  typeof data.resolve === 'function' &&
47
45
  typeof data.reject === 'function') {
48
46
  const { task, resolve, reject, delay, id } = data;
49
47
  try {
48
+ if (id && this.#blacklist.has(id)) {
49
+ reject(new Error('The function was canceled on TinyPromiseQueue.'));
50
+ this.#blacklist.delete(id);
51
+ this.#running = false;
52
+ this.#processQueue();
53
+ return;
54
+ }
50
55
  if (delay && id) {
51
56
  await new Promise((resolveDelay) => {
52
57
  const timeoutId = setTimeout(() => {
@@ -56,13 +61,6 @@ class TinyPromiseQueue {
56
61
  this.#timeouts[id] = timeoutId;
57
62
  });
58
63
  }
59
- if (id && this.#blacklist.has(id)) {
60
- reject(new Error('The function was canceled on TinyPromiseQueue.'));
61
- this.#blacklist.delete(id);
62
- this.#running = false;
63
- this.#processQueue();
64
- return;
65
- }
66
64
  const result = await task();
67
65
  resolve(result);
68
66
  }
@@ -75,6 +73,54 @@ class TinyPromiseQueue {
75
73
  }
76
74
  }
77
75
  }
76
+ /**
77
+ * Processes a group task.
78
+ *
79
+ * @returns {Promise<void>}
80
+ */
81
+ async #groupProcessQueue() {
82
+ /** @type {Array<QueuedTask>} */
83
+ const grouped = [];
84
+ while (this.#queue.length && this.#queue[0]?.marker === 'POINT_MARKER') {
85
+ // @ts-ignore
86
+ grouped.push(this.#queue.shift());
87
+ }
88
+ if (grouped.length === 0) {
89
+ this.#running = false;
90
+ this.#processQueue();
91
+ return;
92
+ }
93
+ await Promise.all(grouped.map(({ task, resolve, reject, id }) => new Promise(async (pResolve) => {
94
+ if (id && this.#blacklist.has(id)) {
95
+ this.#blacklist.delete(id);
96
+ reject(new Error('The function was canceled on TinyPromiseQueue.'));
97
+ pResolve(true);
98
+ return;
99
+ }
100
+ await task().then(resolve).catch(reject);
101
+ pResolve(true);
102
+ })));
103
+ this.#running = false;
104
+ this.#processQueue();
105
+ }
106
+ /**
107
+ * Processes the next task in the queue if not already running.
108
+ * Ensures tasks are executed in order, one at a time.
109
+ *
110
+ * @returns {Promise<void>}
111
+ */
112
+ async #processQueue() {
113
+ if (this.#running || this.#queue.length === 0)
114
+ return;
115
+ this.#running = true;
116
+ if (typeof this.#queue[0]?.marker !== 'string' || this.#queue[0]?.marker !== 'POINT_MARKER') {
117
+ const data = this.#queue.shift();
118
+ // @ts-ignore
119
+ this.#normalProcessQueue(data);
120
+ }
121
+ else
122
+ this.#groupProcessQueue();
123
+ }
78
124
  /**
79
125
  * Returns the index of a task by its ID.
80
126
  *
@@ -112,6 +158,22 @@ class TinyPromiseQueue {
112
158
  const [item] = this.#queue.splice(fromIndex, 1);
113
159
  this.#queue.splice(toIndex, 0, item);
114
160
  }
161
+ /**
162
+ * Inserts a point in the queue where subsequent tasks will be grouped and executed together in a Promise.all.
163
+ * If the queue is currently empty, behaves like a regular promise.
164
+ *
165
+ * @param {(...args: any[]) => Promise<any>|Promise<any>} task A function that returns a Promise.
166
+ * @param {string} [id] Optional ID to identify the task in the queue.
167
+ * @returns {Promise<any>} A Promise that resolves or rejects with the result of the task once it's processed.
168
+ */
169
+ async enqueuePoint(task, id) {
170
+ if (!this.#running)
171
+ return task();
172
+ return new Promise((resolve, reject) => {
173
+ this.#queue.push({ marker: 'POINT_MARKER', task, resolve, reject, id });
174
+ this.#processQueue();
175
+ });
176
+ }
115
177
  /**
116
178
  * Adds a new async task to the queue and ensures it runs in order after previous tasks.
117
179
  * Optionally, a delay can be added before the task is executed.
@@ -148,7 +210,8 @@ class TinyPromiseQueue {
148
210
  }
149
211
  const index = this.getIndexById(id);
150
212
  if (index !== -1) {
151
- this.#queue.splice(index, 1);
213
+ const [removed] = this.#queue.splice(index, 1);
214
+ removed?.reject?.(new Error('The function was canceled on TinyPromiseQueue.'));
152
215
  cancelled = true;
153
216
  }
154
217
  if (cancelled)
@@ -36,3 +36,94 @@ toTitleCase('hello world'); // → "Hello World"
36
36
  ```js
37
37
  toTitleCaseLowerFirst('hello world'); // → "hello World"
38
38
  ```
39
+
40
+ ---
41
+
42
+ ## 🎯 `addAiMarkerShortcut(key = 'a')`
43
+
44
+ Enables a keyboard shortcut (`Ctrl + Alt + [key]`) that toggles a CSS class on the `<body>` element. Useful for marking or highlighting AI-generated content dynamically.
45
+
46
+ ---
47
+
48
+ ### 🔤 Syntax
49
+
50
+ ```js
51
+ addAiMarkerShortcut(key)
52
+ ```
53
+
54
+ ---
55
+
56
+ ### 🧾 Parameters
57
+
58
+ | Name | Type | Default | Description |
59
+ | ----- | -------- | ------- | ---------------------------------------------------------------------------- |
60
+ | `key` | `string` | `'a'` | The character key to use in combination with `Ctrl + Alt`. Case-insensitive. |
61
+
62
+ ---
63
+
64
+ ### ⚙️ Behavior
65
+
66
+ * ⌨️ When the user presses `Ctrl + Alt + [key]`, the function toggles the CSS class `detect-made-by-ai` on the `<body>` element.
67
+ * 🧠 The shortcut only works in environments where the DOM is available (e.g., browsers).
68
+ * 🚫 If `document.body` is not available when the shortcut is used (e.g., if the DOM hasn't finished loading), a warning is logged and nothing happens.
69
+
70
+ ---
71
+
72
+ ### ❗ Error Handling
73
+
74
+ Two types of errors are handled:
75
+
76
+ 1. 🧱 **Non-browser environment (e.g., Node.js):**
77
+
78
+ ```
79
+ [AiMarkerShortcut] Environment does not support the DOM. This function must be run in a browser.
80
+ ```
81
+
82
+ 2. 🕓 **DOM not fully loaded at the time of shortcut:**
83
+
84
+ ```
85
+ [AiMarkerShortcut] <body> element not found. Cannot toggle class. Ensure the DOM is fully loaded when using the shortcut.
86
+ ```
87
+
88
+ ---
89
+
90
+ ### 🧪 Example
91
+
92
+ ```js
93
+ addAiMarkerShortcut(); // Uses default key 'a'
94
+ // Pressing Ctrl + Alt + A toggles the class "detect-made-by-ai" on <body>
95
+ ```
96
+
97
+ ---
98
+
99
+ ### 🎨 CSS Integration Example
100
+
101
+ Define the class in your stylesheet to make the toggle visually meaningful:
102
+
103
+ ```css
104
+ body.detect-made-by-ai .ai-content {
105
+ outline: 2px dashed red;
106
+ background-color: rgba(255, 0, 0, 0.05);
107
+ }
108
+ ```
109
+
110
+ ---
111
+
112
+ ### 💡 Tip
113
+
114
+ To avoid the `<body>` warning, make sure you only call the function after the DOM is ready:
115
+
116
+ ```js
117
+ document.addEventListener('DOMContentLoaded', () => {
118
+ addAiMarkerShortcut();
119
+ });
120
+ ```
121
+
122
+ ### 📂 CSS Templates
123
+
124
+ You can use a pre-built CSS template for the `detect-made-by-ai` class, available in the following files:
125
+
126
+ * `/dist/aiMarker.min.css` – A minified version of the CSS for production use.
127
+ * `/dist/aiMarker.css` – The non-minified version for easier readability and customization.
128
+
129
+ Simply include the appropriate file in your project to style the elements marked with the `detect-made-by-ai` class.
@@ -22,6 +22,8 @@ Returns whether the queue is currently processing a task.
22
22
  #### Returns:
23
23
  - `boolean`: `true` if the queue is processing a task, otherwise `false`.
24
24
 
25
+ ---
26
+
25
27
  ### `getIndexById(id)` 🔍
26
28
 
27
29
  Returns the index of a task in the queue by its ID.
@@ -32,6 +34,8 @@ Returns the index of a task in the queue by its ID.
32
34
  #### Returns:
33
35
  - `number`: The index of the task in the queue, or `-1` if not found.
34
36
 
37
+ ---
38
+
35
39
  ### `getQueuedIds()` 📋
36
40
 
37
41
  Returns a list of IDs for all tasks currently in the queue.
@@ -39,6 +43,8 @@ Returns a list of IDs for all tasks currently in the queue.
39
43
  #### Returns:
40
44
  - `Array<{ index: number, id: string }>`: An array of task IDs and their corresponding indices.
41
45
 
46
+ ---
47
+
42
48
  ### `reorderQueue(fromIndex, toIndex)` 🔄
43
49
 
44
50
  Reorders a task in the queue from one index to another.
@@ -50,6 +56,8 @@ Reorders a task in the queue from one index to another.
50
56
  #### Returns:
51
57
  - `void`: This method does not return anything.
52
58
 
59
+ ---
60
+
53
61
  ### `enqueue(task, delay, id)` ⏳
54
62
 
55
63
  Adds a new async task to the queue and ensures it runs in order after previous tasks. Optionally, a delay can be added before the task is executed.
@@ -65,6 +73,26 @@ If the task is canceled before execution, it will be rejected with the message:
65
73
  #### Returns:
66
74
  - `Promise<any>`: A promise that resolves or rejects with the result of the task once it's processed.
67
75
 
76
+ ---
77
+
78
+ ### `enqueuePoint(task, id)` 🪢
79
+
80
+ Adds an async task to a parallel group in the queue. All tasks added with `enqueuePoint` before the next `enqueue` will be executed **simultaneously**, but only after all previous tasks in the queue have completed.
81
+
82
+ These grouped tasks share a "concurrent checkpoint" and will run using `Promise.all`. Each task resolves or rejects independently.
83
+
84
+ If a task is canceled before execution, it will be rejected with the message:
85
+ **"The function was canceled on TinyPromiseQueue."**
86
+
87
+ #### Parameters:
88
+ - `task` (`Function`): A function that returns a `Promise` to be executed sequentially.
89
+ - `id` (`string`): Optional ID to identify the task in the queue.
90
+
91
+ #### Returns:
92
+ - `Promise<any>`: A promise that resolves or rejects with the result of the task once it's processed.
93
+
94
+ ---
95
+
68
96
  ### `cancelTask(id)` ❌
69
97
 
70
98
  Cancels a scheduled delay and removes the task from the queue. Adds the ID to a blacklist so the task is skipped if already being processed.
@@ -113,6 +141,73 @@ setTimeout(() => {
113
141
  }, 60);
114
142
  ```
115
143
 
144
+ ```js
145
+ import { TinyPromiseQueue } from './TinyPromiseQueue';
146
+
147
+ const queue = new TinyPromiseQueue();
148
+ await new Promise((resolve) => {
149
+
150
+ // Task generator with color-coded logs
151
+ function createTask(name, duration = 500) {
152
+ return () =>
153
+ new Promise((resolve) => {
154
+ console.log(`\x1b[34m[STARTED]\x1b[0m \x1b[36m${name}\x1b[0m`);
155
+ setTimeout(() => {
156
+ console.log(`\x1b[32m[FINISHED]\x1b[0m \x1b[36m${name}\x1b[0m`);
157
+ resolve(name);
158
+ }, duration);
159
+ });
160
+ }
161
+
162
+ // Function to simulate a parallel group of enqueues
163
+ const parallelEnqueueGroup = (groupName, delayStart = 0) => {
164
+ const count = Math.floor(Math.random() * (15 - 1 + 1) + 1);
165
+ for (let i = 1; i <= count; i++) {
166
+ const taskName = `${groupName}-${i}`;
167
+ queue.enqueue(createTask(taskName, 300 + i * 50), delayStart, taskName);
168
+ }
169
+ };
170
+
171
+ // Main enqueue block
172
+ queue.enqueue(createTask('Init-1', 400), 50, 'init-1');
173
+ queue.enqueue(createTask('Init-2', 400), 0, 'init-2');
174
+
175
+ // Simulate enqueues from other contexts "in parallel"
176
+ parallelEnqueueGroup('Alpha', 80); // Starts after 80ms
177
+ parallelEnqueueGroup('Beta', 120); // Starts after 120ms
178
+ parallelEnqueueGroup('Gamma', 250); // Starts after 250ms
179
+ parallelEnqueueGroup('Delta', 180); // Starts after 180ms
180
+ parallelEnqueueGroup('Epsilon', 300); // Starts after 300ms
181
+ parallelEnqueueGroup('Zeta', 160); // Starts after 160ms
182
+ parallelEnqueueGroup('Eta', 90); // Starts after 90ms
183
+ parallelEnqueueGroup('Theta', 220); // Starts after 220ms
184
+ parallelEnqueueGroup('Iota', 140); // Starts after 140ms
185
+ parallelEnqueueGroup('Kappa', 400); // Starts after 400ms
186
+ parallelEnqueueGroup('Lambda', 190); // Starts after 190ms
187
+ parallelEnqueueGroup('Mu', 260); // Starts after 260ms
188
+ parallelEnqueueGroup('Nu', 110); // Starts after 110ms
189
+ parallelEnqueueGroup('Xi', 330); // Starts after 330ms
190
+ parallelEnqueueGroup('Omicron', 170); // Starts after 170ms
191
+ parallelEnqueueGroup('Pi', 280); // Starts after 280ms
192
+ parallelEnqueueGroup('Rho', 70); // Starts after 70ms
193
+ parallelEnqueueGroup('Sigma', 360); // Starts after 360ms
194
+ parallelEnqueueGroup('Tau', 130); // Starts after 130ms
195
+ parallelEnqueueGroup('Upsilon', 240); // Starts after 240ms
196
+ parallelEnqueueGroup('Phi', 310); // Starts after 310ms
197
+ parallelEnqueueGroup('Chi', 100); // Starts after 100ms
198
+ parallelEnqueueGroup('Psi', 200); // Starts after 200ms
199
+ parallelEnqueueGroup('Omega', 50); // Starts after 50ms
200
+
201
+ // Final task to confirm everything executed
202
+ setTimeout(() => {
203
+ queue.enqueue(createTask('Finalizer', 500), 0, 'finalizer').then(() => {
204
+ console.log('\x1b[35m[QUEUE COMPLETE]\x1b[0m All tasks have been processed.');
205
+ resolve();
206
+ });
207
+ }, 250);
208
+ });
209
+ ```
210
+
116
211
  ### Explanation 🧩:
117
212
  1. **Task Creation**: Tasks are created with a name and an optional duration (in ms). The task logs its start and finish times 🕰️.
118
213
  2. **Task Execution**: Tasks are enqueued one at a time, ensuring they run in the order they were added 🔁.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-essentials",
3
- "version": "1.5.1",
3
+ "version": "1.7.0",
4
4
  "description": "Collection of small, essential scripts designed to be used across various projects. These simple utilities are crafted for speed, ease of use, and versatility.",
5
5
  "scripts": {
6
6
  "test": "npm run test:mjs && npm run test:cjs && npm run test:js",
@@ -15,9 +15,11 @@
15
15
  "fix:prettier:rollup.config": "prettier --write ./rollup.config.mjs",
16
16
  "fix:prettier:webpack.config": "prettier --write ./webpack.config.mjs",
17
17
  "auto-build": "npm run build",
18
- "build": "tsc -p tsconfig.json && rollup -c && webpack --mode production",
18
+ "build": "npm run build:js && npm run build:css",
19
+ "build:js": "tsc -p tsconfig.json && rollup -c && webpack --mode production",
19
20
  "build-clean": "npm run clean && npm run build",
20
21
  "build-dist": "npm run build",
22
+ "build:css": "sass src/v1/scss/aiMarker.scss dist/aiMarker.css --no-source-map && sass src/v1/scss/aiMarker.scss dist/aiMarker.min.css --no-source-map --style=compressed",
21
23
  "clean": "rm -rf dist",
22
24
  "prepublishOnly": "npm run build"
23
25
  },
@@ -80,6 +82,7 @@
80
82
  "prettier": "3.5.3",
81
83
  "rollup": "^4.40.0",
82
84
  "rollup-preserve-directives": "^1.1.3",
85
+ "sass": "^1.87.0",
83
86
  "tinycolor2": "^1.6.0",
84
87
  "tslib": "^2.8.1",
85
88
  "type-fest": "^4.40.0",