tiny-essentials 1.24.5 → 1.25.1

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.
Files changed (37) hide show
  1. package/README.md +8 -8
  2. package/changelog/1/25/0.md +13 -0
  3. package/changelog/1/25/1.md +18 -0
  4. package/dist/v1/TinyAnalogClock.min.js +1 -0
  5. package/dist/v1/TinyBasicsEs.min.js +1 -1
  6. package/dist/v1/TinyEssentials.min.js +1 -1
  7. package/dist/v1/TinyTextDiffer.min.js +1 -0
  8. package/dist/v1/basics/html.cjs +151 -2
  9. package/dist/v1/basics/html.d.mts +109 -0
  10. package/dist/v1/basics/html.mjs +135 -2
  11. package/dist/v1/build/TinyAnalogClock.cjs +7 -0
  12. package/dist/v1/build/TinyAnalogClock.d.mts +3 -0
  13. package/dist/v1/build/TinyAnalogClock.mjs +2 -0
  14. package/dist/v1/build/TinyTextDiffer.cjs +7 -0
  15. package/dist/v1/build/TinyTextDiffer.d.mts +3 -0
  16. package/dist/v1/build/TinyTextDiffer.mjs +2 -0
  17. package/dist/v1/index.cjs +4 -0
  18. package/dist/v1/index.d.mts +3 -1
  19. package/dist/v1/index.mjs +3 -1
  20. package/dist/v1/libs/TinyAnalogClock.cjs +738 -0
  21. package/dist/v1/libs/TinyAnalogClock.d.mts +342 -0
  22. package/dist/v1/libs/TinyAnalogClock.mjs +653 -0
  23. package/dist/v1/libs/TinyRateLimiter.cjs +215 -31
  24. package/dist/v1/libs/TinyRateLimiter.d.mts +179 -25
  25. package/dist/v1/libs/TinyRateLimiter.mjs +215 -31
  26. package/dist/v1/libs/TinyTextDiffer.cjs +288 -0
  27. package/dist/v1/libs/TinyTextDiffer.d.mts +109 -0
  28. package/dist/v1/libs/TinyTextDiffer.mjs +255 -0
  29. package/docs/v1/README.md +5 -8
  30. package/docs/v1/basics/html.md +69 -0
  31. package/docs/v1/libs/TinyAnalogClock.md +295 -0
  32. package/docs/v1/libs/TinyRateLimiter.md +11 -0
  33. package/docs/v1/libs/TinyTextDiffer.md +114 -0
  34. package/package.json +12 -4
  35. package/docs/v1/Ai-Tips.md +0 -51
  36. package/docs/v1/Personal-Ai-Prompts(portuguese).md +0 -134
  37. package/docs/v1/Personal-Ai-Prompts.md +0 -113
@@ -1,44 +1,172 @@
1
- /** @typedef {(groupId: string) => void} OnMemoryExceeded */
2
- /** @typedef {(groupId: string) => void} OnGroupExpired */
3
1
  /**
4
- * Class representing a flexible rate limiter per user or group.
2
+ * Callback triggered when a group's stored hit history exceeds
3
+ * the configured memory limit.
5
4
  *
6
- * This rate limiter supports limiting per user or per group by mapping
7
- * userIds to a common groupId. All users within the same group share
8
- * rate limits.
5
+ * This callback is purely informational and does not block execution.
6
+ *
7
+ * @typedef {(groupId: string) => void} OnMemoryExceeded
8
+ */
9
+ /**
10
+ * Callback triggered when a group is considered expired and removed
11
+ * during the cleanup process.
12
+ *
13
+ * This usually happens when the group remains inactive longer than
14
+ * its configured TTL or the global maxIdle value.
15
+ *
16
+ * @typedef {(groupId: string) => void} OnGroupExpired
17
+ */
18
+ /**
19
+ * A lightweight and flexible rate limiter supporting both user-based
20
+ * and group-based throttling.
21
+ *
22
+ * ## Core Concepts
23
+ * - Every user belongs to a group.
24
+ * - If no explicit group is assigned, the user acts as their own group.
25
+ * - All users within the same group share the same hit history and limits.
26
+ *
27
+ * ## Supported Limiting Strategies
28
+ * - Max number of hits
29
+ * - Time-based sliding window
30
+ * - Combination of both
31
+ *
32
+ * ## Extra Features
33
+ * - Per-group TTL (time-to-live)
34
+ * - Automatic cleanup of inactive groups
35
+ * - Optional memory cap per group
36
+ * - Runtime metrics and statistics
37
+ *
38
+ * ## Interval Window (Sliding Window) Behavior
39
+ *
40
+ * This rate limiter uses a **sliding time window** strategy when `interval`
41
+ * is configured.
42
+ *
43
+ * ### How it works
44
+ * - Each hit is stored as a timestamp (Date.now()).
45
+ * - On every new hit and rate check, timestamps older than:
46
+ *
47
+ * `now - interval`
48
+ *
49
+ * are discarded.
50
+ *
51
+ * - Only hits that occurred within the last `interval` milliseconds
52
+ * are considered valid.
53
+ *
54
+ * ### Practical example
55
+ * If:
56
+ * - interval = 10_000 (10 seconds)
57
+ * - maxHits = 5
58
+ *
59
+ * Then:
60
+ * - Any group may perform **up to 5 hits in any rolling 10-second window**.
61
+ * - The window moves continuously with time.
62
+ *
63
+ * This avoids burst issues common in fixed-window rate limiters.
64
+ *
65
+ * This class is designed to be deterministic, predictable,
66
+ * and safe to use in long-running Node.js processes.
9
67
  */
10
68
  class TinyRateLimiter {
11
- /** @type {number|null} */
69
+ /**
70
+ * Maximum number of timestamps stored per group.
71
+ * When exceeded, older entries are discarded.
72
+ *
73
+ * `null` means unlimited.
74
+ *
75
+ * @type {number|null}
76
+ */
12
77
  #maxMemory = null;
13
- /** @type {NodeJS.Timeout|null} */
78
+ /**
79
+ * Internal timer reference used for periodic cleanup.
80
+ *
81
+ * @type {NodeJS.Timeout|null}
82
+ */
14
83
  #cleanupTimer = null;
15
- /** @type {number|null|undefined} */
84
+ /**
85
+ * Maximum number of allowed hits within the interval window.
86
+ *
87
+ * If `null`, hit count is not enforced.
88
+ *
89
+ * @type {number|null|undefined}
90
+ */
16
91
  #maxHits = null;
17
- /** @type {number|null|undefined} */
92
+ /**
93
+ * Sliding window duration in milliseconds.
94
+ *
95
+ * When defined, the rate limiter operates in **sliding window mode**:
96
+ * only hits that occurred within the last `interval` milliseconds
97
+ * are considered when evaluating limits.
98
+ *
99
+ * If `null`, time-based limiting is disabled and only `maxHits`
100
+ * (if defined) is used.
101
+ *
102
+ * @type {number|null|undefined}
103
+ */
18
104
  #interval = null;
19
- /** @type {number|null|undefined} */
105
+ /**
106
+ * Interval (in milliseconds) at which cleanup runs automatically.
107
+ *
108
+ * If `null`, cleanup must be triggered manually.
109
+ *
110
+ * @type {number|null|undefined}
111
+ */
20
112
  #cleanupInterval = null;
21
- /** @type {number|null|undefined} */
113
+ /**
114
+ * Maximum allowed inactivity time (in milliseconds)
115
+ * before a group is considered expired.
116
+ *
117
+ * @type {number|null|undefined}
118
+ */
22
119
  #maxIdle = null;
23
- /** @type {Map<string, number[]>} */
120
+ /**
121
+ * Stores hit timestamps per group.
122
+ *
123
+ * Key: groupId
124
+ * Value: array of timestamps (ms)
125
+ *
126
+ * @type {Map<string, number[]>}
127
+ */
24
128
  groupData = new Map(); // groupId -> timestamps[]
25
- /** @type {Map<string, number>} */
129
+ /**
130
+ * Stores the timestamp of the most recent activity per group.
131
+ *
132
+ * Used for expiration and cleanup logic.
133
+ *
134
+ * @type {Map<string, number>}
135
+ */
26
136
  lastSeen = new Map(); // groupId -> timestamp
27
- /** @type {Map<string, string>} */
137
+ /**
138
+ * Maps user IDs to their assigned group IDs.
139
+ *
140
+ * @type {Map<string, string>}
141
+ */
28
142
  userToGroup = new Map(); // userId -> groupId
29
- /** @type {Map<string, boolean>} */
143
+ /**
144
+ * Flags whether an ID represents a true group or an implicit user group.
145
+ *
146
+ * `true` → explicit group
147
+ * `false` → implicit (user acting as group)
148
+ *
149
+ * @type {Map<string, boolean>}
150
+ */
30
151
  groupFlags = new Map(); // groupId -> boolean
31
152
  /**
153
+ * Per-group TTL (time-to-live) overrides.
154
+ *
155
+ * If not defined for a group, `maxIdle` is used instead.
156
+ *
32
157
  * @type {Map<string, number>}
33
- * Stores TTL (in ms) for each groupId individually
34
158
  */
35
159
  groupTTL = new Map();
36
160
  /**
161
+ * Callback invoked when a group's memory limit is exceeded.
162
+ *
37
163
  * @type {null|OnMemoryExceeded}
38
164
  */
39
165
  #onMemoryExceeded = null;
40
166
  /**
41
- * Set the callback to be triggered when a group exceeds its limit
167
+ * Assign a callback to be notified when a group's stored
168
+ * hit history exceeds the configured memory limit.
169
+ *
42
170
  * @param {OnMemoryExceeded} callback
43
171
  */
44
172
  setOnMemoryExceeded(callback) {
@@ -47,12 +175,14 @@ class TinyRateLimiter {
47
175
  this.#onMemoryExceeded = callback;
48
176
  }
49
177
  /**
50
- * Clear the onMemoryExceeded callback
178
+ * Removes the memory-exceeded callback.
51
179
  */
52
180
  clearOnMemoryExceeded() {
53
181
  this.#onMemoryExceeded = null;
54
182
  }
55
183
  /**
184
+ * Callback invoked when a group expires and is removed.
185
+ *
56
186
  * @type {null|OnGroupExpired}
57
187
  */
58
188
  #onGroupExpired = null;
@@ -70,18 +200,27 @@ class TinyRateLimiter {
70
200
  this.#onGroupExpired = callback;
71
201
  }
72
202
  /**
73
- * Clear the onGroupExpired callback
203
+ * Removes the group-expiration callback.
74
204
  */
75
205
  clearOnGroupExpired() {
76
206
  this.#onGroupExpired = null;
77
207
  }
78
208
  /**
209
+ * Creates a new TinyRateLimiter instance.
210
+ *
211
+ * At least one of `maxHits` or `interval` must be provided.
212
+ *
79
213
  * @param {Object} options
80
- * @param {number|null} [options.maxMemory] - Max memory allowed
81
- * @param {number} [options.maxHits] - Max interactions allowed
82
- * @param {number} [options.interval] - Time window in milliseconds
83
- * @param {number} [options.cleanupInterval] - Interval for automatic cleanup (ms)
84
- * @param {number} [options.maxIdle=300000] - Max idle time for a user before being cleaned (ms)
214
+ * @param {number|null} [options.maxMemory=100000]
215
+ * Maximum timestamps stored per group (memory cap).
216
+ * @param {number} [options.maxHits]
217
+ * Maximum number of allowed hits.
218
+ * @param {number} [options.interval]
219
+ * Sliding time window in milliseconds.
220
+ * @param {number} [options.cleanupInterval]
221
+ * Interval (ms) for automatic cleanup execution.
222
+ * @param {number} [options.maxIdle=300000]
223
+ * Maximum inactivity time (ms) before a group expires.
85
224
  */
86
225
  constructor({ maxHits, interval, cleanupInterval, maxIdle = 300000, maxMemory = 100000 }) {
87
226
  /** @param {number|undefined} val */
@@ -166,10 +305,14 @@ class TinyRateLimiter {
166
305
  this.groupTTL.delete(groupId);
167
306
  }
168
307
  /**
169
- * Assign a userId to a groupId, with merge if user has existing data.
308
+ * Assigns a user to a group.
309
+ *
310
+ * If the user already has recorded hits, those hits
311
+ * are merged into the target group.
312
+ *
170
313
  * @param {string} userId
171
314
  * @param {string} groupId
172
- * @throws {Error} If userId is already assigned to a different group
315
+ * @throws {Error} If the user belongs to another group
173
316
  */
174
317
  assignToGroup(userId, groupId) {
175
318
  const existingGroup = this.userToGroup.get(userId);
@@ -211,7 +354,11 @@ class TinyRateLimiter {
211
354
  this.groupFlags.set(groupId, true);
212
355
  }
213
356
  /**
214
- * Get the groupId for a given userId
357
+ * Resolves the effective group ID for a user.
358
+ *
359
+ * If the user is not explicitly assigned to a group,
360
+ * the user ID itself is treated as the group ID.
361
+ *
215
362
  * @param {string} userId
216
363
  * @returns {string}
217
364
  */
@@ -219,7 +366,25 @@ class TinyRateLimiter {
219
366
  return this.userToGroup.get(userId) || userId; // fallback: use userId as own group
220
367
  }
221
368
  /**
222
- * Register a hit for a specific user
369
+ * Registers a hit for a user and applies the sliding window logic.
370
+ *
371
+ * ⚠️ **Important usage notice**
372
+ * This method **must be called before** `isRateLimited(userId)`
373
+ * in order for rate limit checks to work correctly.
374
+ *
375
+ * ### Sliding window cleanup
376
+ * When `interval` is configured:
377
+ * - The current timestamp is added to the group's history.
378
+ * - All timestamps older than `now - interval` are immediately removed.
379
+ *
380
+ * This ensures that the stored history always represents
381
+ * the **current active window**.
382
+ *
383
+ * ### Important notes
384
+ * - Cleanup happens on every hit, not on a fixed schedule.
385
+ * - The window continuously moves forward in time.
386
+ * - Memory usage is naturally bounded by time, and optionally by `maxMemory`.
387
+ *
223
388
  * @param {string} userId
224
389
  */
225
390
  hit(userId) {
@@ -252,7 +417,22 @@ class TinyRateLimiter {
252
417
  }
253
418
  }
254
419
  /**
255
- * Check if the user (via their group) is currently rate limited
420
+ * Checks whether a user is currently rate limited.
421
+ *
422
+ * ### Evaluation process
423
+ * When `interval` is defined:
424
+ * 1. The cutoff time is calculated as:
425
+ *
426
+ * `Date.now() - interval`
427
+ *
428
+ * 2. Only hits newer than this cutoff are counted.
429
+ * 3. If `maxHits` is defined:
430
+ * - The group is limited when the count exceeds `maxHits`.
431
+ * 4. If `maxHits` is not defined:
432
+ * - Any hit within the window causes a limited state.
433
+ *
434
+ * This guarantees consistent behavior regardless of when hits occur.
435
+ *
256
436
  * @param {string} userId
257
437
  * @returns {boolean}
258
438
  */
@@ -389,7 +569,11 @@ class TinyRateLimiter {
389
569
  return Object.fromEntries(this.userToGroup);
390
570
  }
391
571
  /**
392
- * Get the interval window in milliseconds.
572
+ * Returns the configured sliding window size in milliseconds.
573
+ *
574
+ * This value represents how far back in time hits are considered
575
+ * valid for rate limiting decisions.
576
+ *
393
577
  * @returns {number}
394
578
  */
395
579
  getInterval() {
@@ -0,0 +1,288 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Represents the result of a single text segment comparison.
5
+ * @typedef {Object} DiffResult
6
+ * @property {string} value - The actual string content of the segment.
7
+ * @property {"normal"|"added"|"deleted"} type - The status of the segment relative to the comparison.
8
+ */
9
+
10
+ /**
11
+ * A utility class to store a history of text strings and compute the differences
12
+ * between any two versions, generating a detailed diff output.
13
+ */
14
+ class TinyTextDiffer {
15
+ /** @type {string[]} */
16
+ #history = [];
17
+
18
+ /** @type {boolean} */
19
+ #destroyed = false;
20
+
21
+ /**
22
+ * Retrieves a shallow copy of the current history array.
23
+ * @returns {string[]}
24
+ */
25
+ get history() {
26
+ return [...this.#history];
27
+ }
28
+
29
+ /**
30
+ * Overwrites the current history array, ensuring all elements are valid strings.
31
+ * @param {string[]} history
32
+ */
33
+ set history(history) {
34
+ if (!Array.isArray(history)) {
35
+ throw new TypeError('History data must be provided as an array.');
36
+ }
37
+ if (!history.every((item) => typeof item === 'string')) {
38
+ throw new TypeError('All items in the history array must be strings.');
39
+ }
40
+ /** @type {string[]} */
41
+ this.#history = [...history];
42
+ }
43
+
44
+ /**
45
+ * Gets the total number of versions currently stored in the history.
46
+ * @returns {number}
47
+ */
48
+ get size() {
49
+ return this.#history.length;
50
+ }
51
+
52
+ /**
53
+ * Initializes a new instance of TinyTextDiffer.
54
+ * @param {string[]} [history=[]]
55
+ */
56
+ constructor(history = []) {
57
+ this.history = history;
58
+ }
59
+
60
+ /**
61
+ * @throws {Error}
62
+ * @returns {void}
63
+ */
64
+ #checkDestroyed() {
65
+ if (this.#destroyed) {
66
+ throw new Error('Cannot perform operations on a destroyed TinyTextDiffer instance.');
67
+ }
68
+ }
69
+
70
+ /**
71
+ * @param {any} index
72
+ * @throws {TypeError}
73
+ * @returns {void}
74
+ */
75
+ #checkIndex(index) {
76
+ if (typeof index !== 'number') {
77
+ throw new TypeError('The provided index must be a valid number.');
78
+ }
79
+ }
80
+
81
+ /**
82
+ * @param {any} text
83
+ * @throws {TypeError}
84
+ * @returns {void}
85
+ */
86
+ #checkText(text) {
87
+ if (typeof text !== 'string') {
88
+ throw new TypeError('The provided text must be a valid string.');
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Retrieves the text string at the specified index.
94
+ * @param {number} index
95
+ * @returns {string}
96
+ */
97
+ get(index) {
98
+ this.#checkDestroyed();
99
+ this.#checkIndex(index);
100
+ if (typeof this.#history[index] === 'undefined') {
101
+ throw new Error(`No text version found at index ${index}.`);
102
+ }
103
+ return this.#history[index];
104
+ }
105
+
106
+ /**
107
+ * Checks if a text string exists at the specified index.
108
+ * @param {number} index
109
+ * @returns {boolean}
110
+ */
111
+ has(index) {
112
+ this.#checkDestroyed();
113
+ this.#checkIndex(index);
114
+ return typeof this.#history[index] !== 'undefined';
115
+ }
116
+
117
+ /**
118
+ * Appends a new text string to the end of the history.
119
+ * @param {string} text
120
+ * @returns {void}
121
+ */
122
+ add(text) {
123
+ this.#checkDestroyed();
124
+ this.#checkText(text);
125
+ this.#history.push(text);
126
+ }
127
+
128
+ /**
129
+ * Removes and returns the last text string from the history.
130
+ * @returns {string | undefined}
131
+ */
132
+ remove() {
133
+ this.#checkDestroyed();
134
+ return this.#history.pop();
135
+ }
136
+
137
+ /**
138
+ * Inserts a text string at the specified index position.
139
+ * @param {number} index
140
+ * @param {string} text
141
+ * @returns {void}
142
+ */
143
+ addAt(index, text) {
144
+ this.#checkDestroyed();
145
+ this.#checkIndex(index);
146
+ this.#checkText(text);
147
+ this.#history.splice(index, 0, text);
148
+ }
149
+
150
+ /**
151
+ * Removes the text string at the specified index position.
152
+ * @param {number} index
153
+ * @returns {boolean}
154
+ */
155
+ removeAt(index) {
156
+ this.#checkDestroyed();
157
+ this.#checkIndex(index);
158
+ const oldSize = this.#history.length;
159
+ this.#history.splice(index, 1);
160
+ const newSize = this.#history.length;
161
+ return oldSize !== newSize;
162
+ }
163
+
164
+ /**
165
+ * Empties the entire history.
166
+ * @returns {void}
167
+ */
168
+ clear() {
169
+ this.#checkDestroyed();
170
+ this.#history = [];
171
+ }
172
+
173
+ /**
174
+ * Compares multiple pairs of text strings from the history based on their indices.
175
+ * Each pair of indices (e.g., index1 and index2) will produce a DiffResult array.
176
+ * @param {...number} indexes - An even number of indices to be compared in pairs.
177
+ * @returns {(DiffResult[])[]} An array of DiffResult arrays for each compared pair.
178
+ */
179
+ compare(...indexes) {
180
+ this.#checkDestroyed();
181
+
182
+ /** @type {number} */
183
+ const totalIndexes = indexes.length;
184
+
185
+ if (totalIndexes === 0 || totalIndexes % 2 !== 0) {
186
+ throw new Error(
187
+ 'The compare method requires an even number of indices to form comparison pairs.',
188
+ );
189
+ }
190
+
191
+ /** @type {(DiffResult[])[]} */
192
+ const results = [];
193
+
194
+ for (let i = 0; i < totalIndexes; i += 2) {
195
+ const i2 = i + 1;
196
+ if (i2 > totalIndexes - 1) continue;
197
+ this.#checkIndex(indexes[i]);
198
+ this.#checkIndex(indexes[i2]);
199
+
200
+ /** @type {string} */
201
+ const str1 = this.#history[indexes[i]];
202
+ /** @type {string} */
203
+ const str2 = this.#history[indexes[i2]];
204
+
205
+ results.push(this._computeDiff(str1, str2));
206
+ }
207
+
208
+ return results;
209
+ }
210
+
211
+ /**
212
+ * Computes the Longest Common Subsequence (LCS) to generate the diff result.
213
+ * @private
214
+ * @param {string} str1
215
+ * @param {string} str2
216
+ * @returns {DiffResult[]}
217
+ */
218
+ _computeDiff(str1, str2) {
219
+ /** @type {number} */
220
+ const len1 = str1.length;
221
+ /** @type {number} */
222
+ const len2 = str2.length;
223
+
224
+ /** @type {number[][]} */
225
+ const matrix = Array(len1 + 1)
226
+ .fill(null)
227
+ .map(() => Array(len2 + 1).fill(0));
228
+
229
+ /** @type {number} */
230
+ let i = 1;
231
+ /** @type {number} */
232
+ let j = 1;
233
+
234
+ for (i = 1; i <= len1; i++) {
235
+ for (j = 1; j <= len2; j++) {
236
+ if (str1[i - 1] === str2[j - 1]) {
237
+ matrix[i][j] = matrix[i - 1][j - 1] + 1;
238
+ } else {
239
+ matrix[i][j] = Math.max(matrix[i - 1][j], matrix[i][j - 1]);
240
+ }
241
+ }
242
+ }
243
+
244
+ i = len1;
245
+ j = len2;
246
+
247
+ /** @type {DiffResult[]} */
248
+ const result = [];
249
+
250
+ while (i > 0 || j > 0) {
251
+ if (i > 0 && j > 0 && str1[i - 1] === str2[j - 1]) {
252
+ result.unshift({ value: str1[i - 1], type: 'normal' });
253
+ i--;
254
+ j--;
255
+ } else if (j > 0 && (i === 0 || matrix[i][j - 1] >= matrix[i - 1][j])) {
256
+ result.unshift({ value: str2[j - 1], type: 'added' });
257
+ j--;
258
+ } else if (i > 0 && (j === 0 || matrix[i][j - 1] < matrix[i - 1][j])) {
259
+ result.unshift({ value: str1[i - 1], type: 'deleted' });
260
+ i--;
261
+ }
262
+ }
263
+
264
+ return result.reduce((/** @type {DiffResult[]} */ acc, /** @type {DiffResult} */ curr) => {
265
+ /** @type {number} */
266
+ const lastIndex = acc.length - 1;
267
+
268
+ if (acc.length > 0 && acc[lastIndex].type === curr.type) {
269
+ acc[lastIndex].value += curr.value;
270
+ } else {
271
+ acc.push(curr);
272
+ }
273
+ return acc;
274
+ }, []);
275
+ }
276
+
277
+ /**
278
+ * Cleans up internal references and marks the instance as destroyed to prevent memory leaks.
279
+ * @returns {void}
280
+ */
281
+ destroy() {
282
+ if (this.#destroyed) return;
283
+ this.#history = [];
284
+ this.#destroyed = true;
285
+ }
286
+ }
287
+
288
+ module.exports = TinyTextDiffer;