n8n-nodes-message-debounce 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +141 -0
  3. package/dist/credentials/RedisApi.credentials.d.ts +16 -0
  4. package/dist/credentials/RedisApi.credentials.d.ts.map +1 -0
  5. package/dist/credentials/RedisApi.credentials.js +70 -0
  6. package/dist/credentials/RedisApi.credentials.js.map +1 -0
  7. package/dist/credentials/redisApi.svg +17 -0
  8. package/dist/nodes/MessageDebounce/MessageDebounce.description.d.ts +8 -0
  9. package/dist/nodes/MessageDebounce/MessageDebounce.description.d.ts.map +1 -0
  10. package/dist/nodes/MessageDebounce/MessageDebounce.description.js +247 -0
  11. package/dist/nodes/MessageDebounce/MessageDebounce.description.js.map +1 -0
  12. package/dist/nodes/MessageDebounce/MessageDebounce.node.d.ts +19 -0
  13. package/dist/nodes/MessageDebounce/MessageDebounce.node.d.ts.map +1 -0
  14. package/dist/nodes/MessageDebounce/MessageDebounce.node.js +102 -0
  15. package/dist/nodes/MessageDebounce/MessageDebounce.node.js.map +1 -0
  16. package/dist/nodes/MessageDebounce/constants.d.ts +17 -0
  17. package/dist/nodes/MessageDebounce/constants.d.ts.map +1 -0
  18. package/dist/nodes/MessageDebounce/constants.js +42 -0
  19. package/dist/nodes/MessageDebounce/constants.js.map +1 -0
  20. package/dist/nodes/MessageDebounce/execute.d.ts +13 -0
  21. package/dist/nodes/MessageDebounce/execute.d.ts.map +1 -0
  22. package/dist/nodes/MessageDebounce/execute.js +217 -0
  23. package/dist/nodes/MessageDebounce/execute.js.map +1 -0
  24. package/dist/nodes/MessageDebounce/helpers.d.ts +19 -0
  25. package/dist/nodes/MessageDebounce/helpers.d.ts.map +1 -0
  26. package/dist/nodes/MessageDebounce/helpers.js +43 -0
  27. package/dist/nodes/MessageDebounce/helpers.js.map +1 -0
  28. package/dist/nodes/MessageDebounce/messageDebounce.svg +17 -0
  29. package/dist/nodes/MessageDebounce/types.d.ts +30 -0
  30. package/dist/nodes/MessageDebounce/types.d.ts.map +1 -0
  31. package/dist/nodes/MessageDebounce/types.js +7 -0
  32. package/dist/nodes/MessageDebounce/types.js.map +1 -0
  33. package/dist/nodes/MessageDebounce/utils/RedisClient.d.ts +59 -0
  34. package/dist/nodes/MessageDebounce/utils/RedisClient.d.ts.map +1 -0
  35. package/dist/nodes/MessageDebounce/utils/RedisClient.js +270 -0
  36. package/dist/nodes/MessageDebounce/utils/RedisClient.js.map +1 -0
  37. package/package.json +58 -0
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ /**
3
+ * MessageDebounce n8n community node — entry point.
4
+ *
5
+ * Intentionally thin: this file wires together the separated modules.
6
+ *
7
+ * Module map:
8
+ * description.ts → INodeTypeDescription (UI definition)
9
+ * execute.ts → runDebounce() (core debounce business logic)
10
+ * types.ts → shared interfaces and types
11
+ * constants.ts → Lua script, TTL multipliers
12
+ * helpers.ts → pure utility functions
13
+ * utils/RedisClient.ts → native RESP2 Redis client (zero external deps)
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.MessageDebounce = void 0;
17
+ const n8n_workflow_1 = require("n8n-workflow");
18
+ const MessageDebounce_description_1 = require("./MessageDebounce.description");
19
+ const execute_1 = require("./execute");
20
+ const RedisClient_1 = require("./utils/RedisClient");
21
+ // eslint-disable-next-line @n8n/community-nodes/icon-validation -- icon is defined in MessageDebounce.description.ts
22
+ class MessageDebounce {
23
+ constructor() {
24
+ this.description = MessageDebounce_description_1.description;
25
+ }
26
+ async execute() {
27
+ const itemIndex = 0;
28
+ // ------------------------------------------------------------------
29
+ // 1. Validate required parameters
30
+ // ------------------------------------------------------------------
31
+ const sessionId = this.getNodeParameter('sessionId', itemIndex, '').trim();
32
+ const message = this.getNodeParameter('message', itemIndex, '');
33
+ const debounceWindowRaw = this.getNodeParameter('debounceWindow', itemIndex, null);
34
+ if (!sessionId) {
35
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Session ID is required', {
36
+ itemIndex,
37
+ description: 'Set the Session ID field to a unique identifier for this conversation.',
38
+ });
39
+ }
40
+ if (!message && message !== '0') {
41
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Message is required', {
42
+ itemIndex,
43
+ description: 'Set the Message field to the content you want to buffer.',
44
+ });
45
+ }
46
+ if (debounceWindowRaw === null || debounceWindowRaw === '' || debounceWindowRaw === undefined) {
47
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Debounce Window is required', {
48
+ itemIndex,
49
+ description: 'Enter the number of seconds to wait for silence before flushing messages.',
50
+ });
51
+ }
52
+ const debounceWindowSec = Number(debounceWindowRaw);
53
+ if (isNaN(debounceWindowSec) || debounceWindowSec < 1) {
54
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Debounce Window must be a positive number (minimum 1 second)', { itemIndex });
55
+ }
56
+ // ------------------------------------------------------------------
57
+ // 2. Resolve options
58
+ // ------------------------------------------------------------------
59
+ const raw = this.getNodeParameter('options', itemIndex, {});
60
+ const opts = {
61
+ maxMessages: Number(raw.maxMessages ?? 0),
62
+ maxWaitTimeSec: Number(raw.maxWaitTime ?? 0),
63
+ separator: raw.separator !== undefined ? raw.separator : '\n',
64
+ onDuplicate: raw.onDuplicateMessage ?? 'include',
65
+ firstMsgBehavior: raw.firstMessageBehavior ?? 'none',
66
+ firstMsgCustomWindow: Number(raw.firstMessageCustomWindow ?? 3),
67
+ sessionTtlUnit: raw.sessionTtlUnit ?? 'never',
68
+ sessionTtlValue: Number(raw.sessionTtlValue ?? 30),
69
+ flushKeywords: (raw.flushKeywords?.keywords ?? [])
70
+ .map((k) => k.keyword?.trim().toLowerCase())
71
+ .filter(Boolean),
72
+ };
73
+ // ------------------------------------------------------------------
74
+ // 3. Connect to Redis
75
+ // ------------------------------------------------------------------
76
+ let credentials;
77
+ try {
78
+ credentials = (await this.getCredentials('redisApi'));
79
+ }
80
+ catch (err) {
81
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Redis credentials error: ${err instanceof Error ? err.message : String(err)}`, { itemIndex });
82
+ }
83
+ const redis = new RedisClient_1.RedisClient(credentials);
84
+ try {
85
+ await redis.connect();
86
+ }
87
+ catch (err) {
88
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Redis connection failed: ${err instanceof Error ? err.message : String(err)}`, { itemIndex });
89
+ }
90
+ // ------------------------------------------------------------------
91
+ // 4. Run debounce logic (see execute.ts)
92
+ // ------------------------------------------------------------------
93
+ try {
94
+ return await (0, execute_1.runDebounce)(redis, sessionId, message, debounceWindowSec, opts, itemIndex);
95
+ }
96
+ finally {
97
+ redis.disconnect();
98
+ }
99
+ }
100
+ }
101
+ exports.MessageDebounce = MessageDebounce;
102
+ //# sourceMappingURL=MessageDebounce.node.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MessageDebounce.node.js","sourceRoot":"","sources":["../../../nodes/MessageDebounce/MessageDebounce.node.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Lua script for atomic compare-and-flush in Redis.
3
+ *
4
+ * KEYS[1] = messages list key (debounce:{sessionId}:msgs)
5
+ * KEYS[2] = startTime key (debounce:{sessionId}:startTime)
6
+ * ARGV[1] = beforeId (UUID of the message pushed by this execution)
7
+ *
8
+ * Returns: array of raw JSON message entries if this execution wins the race,
9
+ * or nil if another message arrived after this one (we are not the last).
10
+ *
11
+ * The script is atomic — executed by a single Redis server command, so no
12
+ * race conditions can occur between the LINDEX check and the LRANGE+DEL.
13
+ */
14
+ export declare const FLUSH_SCRIPT = "\nlocal key = KEYS[1]\nlocal tkey = KEYS[2]\nlocal bid = ARGV[1]\n\nlocal last = redis.call('LINDEX', key, -1)\nif not last then return nil end\n\nlocal ok, entry = pcall(cjson.decode, last)\nif not ok then return nil end\nif entry.id ~= bid then return nil end\n\nlocal all = redis.call('LRANGE', key, 0, -1)\nredis.call('DEL', key)\nredis.call('DEL', tkey)\nreturn all\n";
15
+ /** Session TTL multipliers: converts a user-facing unit to seconds. */
16
+ export declare const TTL_MULTIPLIERS: Record<string, number>;
17
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../nodes/MessageDebounce/constants.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,YAAY,2XAgBxB,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAMlD,CAAC"}
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TTL_MULTIPLIERS = exports.FLUSH_SCRIPT = void 0;
4
+ /**
5
+ * Lua script for atomic compare-and-flush in Redis.
6
+ *
7
+ * KEYS[1] = messages list key (debounce:{sessionId}:msgs)
8
+ * KEYS[2] = startTime key (debounce:{sessionId}:startTime)
9
+ * ARGV[1] = beforeId (UUID of the message pushed by this execution)
10
+ *
11
+ * Returns: array of raw JSON message entries if this execution wins the race,
12
+ * or nil if another message arrived after this one (we are not the last).
13
+ *
14
+ * The script is atomic — executed by a single Redis server command, so no
15
+ * race conditions can occur between the LINDEX check and the LRANGE+DEL.
16
+ */
17
+ exports.FLUSH_SCRIPT = `
18
+ local key = KEYS[1]
19
+ local tkey = KEYS[2]
20
+ local bid = ARGV[1]
21
+
22
+ local last = redis.call('LINDEX', key, -1)
23
+ if not last then return nil end
24
+
25
+ local ok, entry = pcall(cjson.decode, last)
26
+ if not ok then return nil end
27
+ if entry.id ~= bid then return nil end
28
+
29
+ local all = redis.call('LRANGE', key, 0, -1)
30
+ redis.call('DEL', key)
31
+ redis.call('DEL', tkey)
32
+ return all
33
+ `;
34
+ /** Session TTL multipliers: converts a user-facing unit to seconds. */
35
+ exports.TTL_MULTIPLIERS = {
36
+ minutes: 60,
37
+ hours: 3600,
38
+ days: 86400,
39
+ months: 30 * 86400, // approximate (30 days)
40
+ years: 365 * 86400, // approximate (365 days)
41
+ };
42
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../../../nodes/MessageDebounce/constants.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;GAYG;AACU,QAAA,YAAY,GAAG;;;;;;;;;;;;;;;;CAgB3B,CAAC;AAEF,uEAAuE;AAC1D,QAAA,eAAe,GAA2B;IACnD,OAAO,EAAE,EAAE;IACX,KAAK,EAAE,IAAK;IACZ,IAAI,EAAE,KAAM;IACZ,MAAM,EAAE,EAAE,GAAG,KAAM,EAAI,wBAAwB;IAC/C,KAAK,EAAE,GAAG,GAAG,KAAM,EAAI,yBAAyB;CACnD,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Core debounce execution flow for the MessageDebounce node.
3
+ *
4
+ * Extracted from MessageDebounce.node.ts so that execute() (which TypeScript
5
+ * types as `this: IExecuteFunctions`) can call it cleanly as a standalone function.
6
+ *
7
+ * All business logic for buffering, flushing, and race-condition handling lives here.
8
+ */
9
+ import type { INodeExecutionData } from 'n8n-workflow';
10
+ import type { ResolvedOptions } from './types';
11
+ import type { RedisClient } from './utils/RedisClient';
12
+ export declare function runDebounce(redis: RedisClient, sessionId: string, message: string, debounceWindowSec: number, opts: ResolvedOptions, itemIndex: number): Promise<INodeExecutionData[][]>;
13
+ //# sourceMappingURL=execute.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"execute.d.ts","sourceRoot":"","sources":["../../../nodes/MessageDebounce/execute.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAe,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAIpE,OAAO,KAAK,EAA6C,eAAe,EAAE,MAAM,SAAS,CAAC;AAC1F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD,wBAAsB,WAAW,CAC7B,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,iBAAiB,EAAE,MAAM,EACzB,IAAI,EAAE,eAAe,EACrB,SAAS,EAAE,MAAM,GAClB,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC,CA4MjC"}
@@ -0,0 +1,217 @@
1
+ "use strict";
2
+ /**
3
+ * Core debounce execution flow for the MessageDebounce node.
4
+ *
5
+ * Extracted from MessageDebounce.node.ts so that execute() (which TypeScript
6
+ * types as `this: IExecuteFunctions`) can call it cleanly as a standalone function.
7
+ *
8
+ * All business logic for buffering, flushing, and race-condition handling lives here.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.runDebounce = runDebounce;
12
+ const crypto_1 = require("crypto");
13
+ const constants_1 = require("./constants");
14
+ const helpers_1 = require("./helpers");
15
+ async function runDebounce(redis, sessionId, message, debounceWindowSec, opts, itemIndex) {
16
+ const { msgKey, tsKey } = (0, helpers_1.makeKeys)(sessionId);
17
+ /** Reads all buffered messages, deletes Redis keys, returns n8n output. */
18
+ const flush = async (reason) => {
19
+ let entries;
20
+ try {
21
+ entries = await redis.lrange(msgKey, 0, -1);
22
+ await redis.del(msgKey, tsKey);
23
+ }
24
+ catch (err) {
25
+ throw (0, helpers_1.wrapRedisError)(err);
26
+ }
27
+ const messages = entries.map((raw) => {
28
+ try {
29
+ return JSON.parse(raw).content;
30
+ }
31
+ catch {
32
+ return raw;
33
+ }
34
+ });
35
+ const output = {
36
+ fullMessage: messages.join(opts.separator),
37
+ messageCount: messages.length,
38
+ flushReason: reason,
39
+ };
40
+ return [[{ json: output, pairedItem: { item: itemIndex } }]];
41
+ };
42
+ // -------------------------------------------------------------------------
43
+ // Step 1 — Is this the first message in the session?
44
+ // -------------------------------------------------------------------------
45
+ let currentLen;
46
+ try {
47
+ currentLen = await redis.llen(msgKey);
48
+ }
49
+ catch (err) {
50
+ throw (0, helpers_1.wrapRedisError)(err);
51
+ }
52
+ const isFirstMessage = currentLen === 0;
53
+ // -------------------------------------------------------------------------
54
+ // Step 2 — First-message special behaviors
55
+ // -------------------------------------------------------------------------
56
+ if (isFirstMessage && opts.firstMsgBehavior === 'immediate') {
57
+ const entry = { id: (0, crypto_1.randomUUID)(), content: message };
58
+ try {
59
+ await redis.rpush(msgKey, JSON.stringify(entry));
60
+ }
61
+ catch (err) {
62
+ throw (0, helpers_1.wrapRedisError)(err);
63
+ }
64
+ return flush('firstMessage');
65
+ }
66
+ // -------------------------------------------------------------------------
67
+ // Step 3 — Duplicate message handling
68
+ // -------------------------------------------------------------------------
69
+ if (opts.onDuplicate !== 'include') {
70
+ let lastRaw = null;
71
+ try {
72
+ lastRaw = await redis.lindex(msgKey, -1);
73
+ }
74
+ catch (err) {
75
+ throw (0, helpers_1.wrapRedisError)(err);
76
+ }
77
+ if (lastRaw !== null) {
78
+ let lastEntry = null;
79
+ try {
80
+ lastEntry = JSON.parse(lastRaw);
81
+ }
82
+ catch {
83
+ // malformed entry — treat as not duplicate
84
+ }
85
+ if (lastEntry?.content === message) {
86
+ if (opts.onDuplicate === 'ignore')
87
+ return [[]];
88
+ if (opts.onDuplicate === 'flush') {
89
+ const entry = { id: (0, crypto_1.randomUUID)(), content: message };
90
+ try {
91
+ await redis.rpush(msgKey, JSON.stringify(entry));
92
+ }
93
+ catch (err) {
94
+ throw (0, helpers_1.wrapRedisError)(err);
95
+ }
96
+ return flush('duplicate');
97
+ }
98
+ }
99
+ }
100
+ }
101
+ // -------------------------------------------------------------------------
102
+ // Step 4 — Flush keywords
103
+ // -------------------------------------------------------------------------
104
+ if (opts.flushKeywords.length > 0) {
105
+ const lower = message.toLowerCase();
106
+ if (opts.flushKeywords.some((kw) => lower.includes(kw))) {
107
+ const entry = { id: (0, crypto_1.randomUUID)(), content: message };
108
+ try {
109
+ await redis.rpush(msgKey, JSON.stringify(entry));
110
+ }
111
+ catch (err) {
112
+ throw (0, helpers_1.wrapRedisError)(err);
113
+ }
114
+ return flush('keyword');
115
+ }
116
+ }
117
+ // -------------------------------------------------------------------------
118
+ // Step 5 — Push message + record session metadata
119
+ // -------------------------------------------------------------------------
120
+ const entry = { id: (0, crypto_1.randomUUID)(), content: message };
121
+ let newLen;
122
+ try {
123
+ newLen = await redis.rpush(msgKey, JSON.stringify(entry));
124
+ }
125
+ catch (err) {
126
+ throw (0, helpers_1.wrapRedisError)(err);
127
+ }
128
+ // Record start time only once per session (NX flag).
129
+ try {
130
+ await redis.set(tsKey, String(Date.now()), { nx: true });
131
+ }
132
+ catch (err) {
133
+ throw (0, helpers_1.wrapRedisError)(err);
134
+ }
135
+ // Apply Session TTL (0 = never expire).
136
+ const ttlSec = (0, helpers_1.toTtlSeconds)(opts.sessionTtlValue, opts.sessionTtlUnit);
137
+ if (ttlSec > 0) {
138
+ try {
139
+ await redis.expire(msgKey, ttlSec);
140
+ await redis.expire(tsKey, ttlSec);
141
+ }
142
+ catch (err) {
143
+ throw (0, helpers_1.wrapRedisError)(err);
144
+ }
145
+ }
146
+ // -------------------------------------------------------------------------
147
+ // Step 6 — Max messages threshold (flush before sleeping)
148
+ // -------------------------------------------------------------------------
149
+ if (opts.maxMessages > 0 && newLen >= opts.maxMessages) {
150
+ return flush('maxMessages');
151
+ }
152
+ // -------------------------------------------------------------------------
153
+ // Step 7 — Sleep (debounce window, racing against maxWait if configured)
154
+ // -------------------------------------------------------------------------
155
+ const windowMs = isFirstMessage && opts.firstMsgBehavior === 'customWindow'
156
+ ? opts.firstMsgCustomWindow * 1000
157
+ : debounceWindowSec * 1000;
158
+ let maxWaitMs = 0;
159
+ if (opts.maxWaitTimeSec > 0) {
160
+ let startTimeRaw = null;
161
+ try {
162
+ startTimeRaw = await redis.get(tsKey);
163
+ }
164
+ catch (err) {
165
+ throw (0, helpers_1.wrapRedisError)(err);
166
+ }
167
+ if (startTimeRaw !== null) {
168
+ const elapsed = Date.now() - parseInt(startTimeRaw, 10);
169
+ maxWaitMs = Math.max(0, opts.maxWaitTimeSec * 1000 - elapsed);
170
+ }
171
+ }
172
+ let timedOutByMaxWait = false;
173
+ if (opts.maxWaitTimeSec > 0 && maxWaitMs <= windowMs) {
174
+ await Promise.race([
175
+ (0, helpers_1.sleep)(windowMs),
176
+ (0, helpers_1.sleep)(maxWaitMs).then(() => {
177
+ timedOutByMaxWait = true;
178
+ }),
179
+ ]);
180
+ }
181
+ else {
182
+ await (0, helpers_1.sleep)(windowMs);
183
+ }
184
+ if (timedOutByMaxWait) {
185
+ return flush('maxWaitTime');
186
+ }
187
+ // -------------------------------------------------------------------------
188
+ // Step 8 — Atomic Lua compare-and-flush
189
+ // -------------------------------------------------------------------------
190
+ let luaResult;
191
+ try {
192
+ luaResult = await redis.eval(constants_1.FLUSH_SCRIPT, [msgKey, tsKey], [entry.id]);
193
+ }
194
+ catch (err) {
195
+ throw (0, helpers_1.wrapRedisError)(err);
196
+ }
197
+ if (luaResult === null || !Array.isArray(luaResult)) {
198
+ // A newer message arrived after ours — stop the flow silently, no output.
199
+ return [[]];
200
+ }
201
+ // We won the race — emit the full consolidated message.
202
+ const messages = luaResult.map((raw) => {
203
+ try {
204
+ return JSON.parse(raw).content;
205
+ }
206
+ catch {
207
+ return raw;
208
+ }
209
+ });
210
+ const output = {
211
+ fullMessage: messages.join(opts.separator),
212
+ messageCount: messages.length,
213
+ flushReason: 'debounceWindow',
214
+ };
215
+ return [[{ json: output, pairedItem: { item: itemIndex } }]];
216
+ }
217
+ //# sourceMappingURL=execute.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"execute.js","sourceRoot":"","sources":["../../../nodes/MessageDebounce/execute.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Pure helper functions for the MessageDebounce node.
3
+ * No n8n or Redis imports here — just plain TypeScript utilities.
4
+ */
5
+ /** Promise-based sleep using setInterval (avoids the restricted `setTimeout` global). */
6
+ export declare function sleep(ms: number): Promise<void>;
7
+ /** Returns the Redis key names for a given session ID. */
8
+ export declare function makeKeys(sessionId: string): {
9
+ msgKey: string;
10
+ tsKey: string;
11
+ };
12
+ /** Wraps a Redis error with a user-friendly prefix. */
13
+ export declare function wrapRedisError(err: unknown): Error;
14
+ /**
15
+ * Converts a user-facing TTL (value + unit) to seconds.
16
+ * Returns 0 when unit is 'never' (no expiry).
17
+ */
18
+ export declare function toTtlSeconds(value: number, unit: string): number;
19
+ //# sourceMappingURL=helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../nodes/MessageDebounce/helpers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,yFAAyF;AACzF,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAO/C;AAED,0DAA0D;AAC1D,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAK7E;AAED,uDAAuD;AACvD,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,KAAK,CAGlD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAIhE"}
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ /**
3
+ * Pure helper functions for the MessageDebounce node.
4
+ * No n8n or Redis imports here — just plain TypeScript utilities.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.sleep = sleep;
8
+ exports.makeKeys = makeKeys;
9
+ exports.wrapRedisError = wrapRedisError;
10
+ exports.toTtlSeconds = toTtlSeconds;
11
+ const constants_1 = require("./constants");
12
+ /** Promise-based sleep using setInterval (avoids the restricted `setTimeout` global). */
13
+ function sleep(ms) {
14
+ return new Promise((resolve) => {
15
+ const handle = setInterval(() => {
16
+ clearInterval(handle);
17
+ resolve();
18
+ }, ms);
19
+ });
20
+ }
21
+ /** Returns the Redis key names for a given session ID. */
22
+ function makeKeys(sessionId) {
23
+ return {
24
+ msgKey: `debounce:${sessionId}:msgs`,
25
+ tsKey: `debounce:${sessionId}:startTime`,
26
+ };
27
+ }
28
+ /** Wraps a Redis error with a user-friendly prefix. */
29
+ function wrapRedisError(err) {
30
+ const msg = err instanceof Error ? err.message : String(err);
31
+ return new Error(`Redis operation failed: ${msg}`);
32
+ }
33
+ /**
34
+ * Converts a user-facing TTL (value + unit) to seconds.
35
+ * Returns 0 when unit is 'never' (no expiry).
36
+ */
37
+ function toTtlSeconds(value, unit) {
38
+ if (unit === 'never')
39
+ return 0;
40
+ const multiplier = constants_1.TTL_MULTIPLIERS[unit] ?? 60;
41
+ return Math.round(value * multiplier);
42
+ }
43
+ //# sourceMappingURL=helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.js","sourceRoot":"","sources":["../../../nodes/MessageDebounce/helpers.ts"],"names":[],"mappings":";AAAA;;;GAGG;;AAKH,sBAOC;AAGD,4BAKC;AAGD,wCAGC;AAMD,oCAIC;AAlCD,2CAA8C;AAE9C,yFAAyF;AACzF,SAAgB,KAAK,CAAC,EAAU;IAC5B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC3B,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,EAAE;YAC5B,aAAa,CAAC,MAAM,CAAC,CAAC;YACtB,OAAO,EAAE,CAAC;QACd,CAAC,EAAE,EAAE,CAAC,CAAC;IACX,CAAC,CAAC,CAAC;AACP,CAAC;AAED,0DAA0D;AAC1D,SAAgB,QAAQ,CAAC,SAAiB;IACtC,OAAO;QACH,MAAM,EAAE,YAAY,SAAS,OAAO;QACpC,KAAK,EAAE,YAAY,SAAS,YAAY;KAC3C,CAAC;AACN,CAAC;AAED,uDAAuD;AACvD,SAAgB,cAAc,CAAC,GAAY;IACvC,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC7D,OAAO,IAAI,KAAK,CAAC,2BAA2B,GAAG,EAAE,CAAC,CAAC;AACvD,CAAC;AAED;;;GAGG;AACH,SAAgB,YAAY,CAAC,KAAa,EAAE,IAAY;IACpD,IAAI,IAAI,KAAK,OAAO;QAAE,OAAO,CAAC,CAAC;IAC/B,MAAM,UAAU,GAAG,2BAAe,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;IAC/C,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,UAAU,CAAC,CAAC;AAC1C,CAAC"}
@@ -0,0 +1,17 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" fill="none">
2
+ <!-- Background circle -->
3
+ <circle cx="30" cy="30" r="28" fill="#FF6B35" opacity="0.12"/>
4
+ <!-- Clock face -->
5
+ <circle cx="30" cy="30" r="20" stroke="#FF6B35" stroke-width="2.5" fill="none"/>
6
+ <!-- Clock hands -->
7
+ <line x1="30" y1="30" x2="30" y2="16" stroke="#FF6B35" stroke-width="2.5" stroke-linecap="round"/>
8
+ <line x1="30" y1="30" x2="40" y2="35" stroke="#FF6B35" stroke-width="2" stroke-linecap="round"/>
9
+ <!-- Center dot -->
10
+ <circle cx="30" cy="30" r="2" fill="#FF6B35"/>
11
+ <!-- Message dots (debounce indicator) -->
12
+ <circle cx="10" cy="48" r="3" fill="#FF6B35" opacity="0.4"/>
13
+ <circle cx="20" cy="52" r="3" fill="#FF6B35" opacity="0.65"/>
14
+ <circle cx="30" cy="54" r="3" fill="#FF6B35" opacity="0.9"/>
15
+ <!-- Arrow suggesting consolidation -->
16
+ <path d="M38 54 L50 54 L46 50 M50 54 L46 58" stroke="#FF6B35" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" opacity="0.8"/>
17
+ </svg>
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Type definitions for the MessageDebounce node.
3
+ * Single source of truth for shared interfaces and types.
4
+ */
5
+ /** Reason that triggered a debounce flush. */
6
+ export type FlushReason = 'debounceWindow' | 'maxMessages' | 'maxWaitTime' | 'keyword' | 'firstMessage' | 'duplicate';
7
+ /** A single buffered message stored in Redis. */
8
+ export interface MessageEntry {
9
+ id: string;
10
+ content: string;
11
+ }
12
+ /** The consolidated output emitted when a flush occurs. */
13
+ export interface DebounceOutput {
14
+ fullMessage: string;
15
+ messageCount: number;
16
+ flushReason: FlushReason;
17
+ }
18
+ /** Resolved options from the node's Options collection. */
19
+ export interface ResolvedOptions {
20
+ maxMessages: number;
21
+ maxWaitTimeSec: number;
22
+ separator: string;
23
+ onDuplicate: string;
24
+ firstMsgBehavior: string;
25
+ firstMsgCustomWindow: number;
26
+ sessionTtlUnit: string;
27
+ sessionTtlValue: number;
28
+ flushKeywords: string[];
29
+ }
30
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../nodes/MessageDebounce/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,8CAA8C;AAC9C,MAAM,MAAM,WAAW,GACjB,gBAAgB,GAChB,aAAa,GACb,aAAa,GACb,SAAS,GACT,cAAc,GACd,WAAW,CAAC;AAElB,iDAAiD;AACjD,MAAM,WAAW,YAAY;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,2DAA2D;AAC3D,MAAM,WAAW,cAAc;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,WAAW,CAAC;CAC5B;AAED,2DAA2D;AAC3D,MAAM,WAAW,eAAe;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,EAAE,CAAC;CAC3B"}
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ /**
3
+ * Type definitions for the MessageDebounce node.
4
+ * Single source of truth for shared interfaces and types.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../nodes/MessageDebounce/types.ts"],"names":[],"mappings":";AAAA;;;GAGG"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Minimal Redis client using the native Node.js `net` module.
3
+ * Implements the RESP2 (REdis Serialization Protocol) specification.
4
+ * Zero external dependencies — required for n8n verified community nodes.
5
+ *
6
+ * Supported commands: AUTH, SELECT, RPUSH, LLEN, LINDEX, LRANGE, DEL, EXPIRE, SET, GET, EVAL
7
+ */
8
+ export interface RedisCredentials {
9
+ host: string;
10
+ port: number;
11
+ username?: string;
12
+ password?: string;
13
+ database?: number;
14
+ tls?: boolean;
15
+ }
16
+ type RespValue = string | number | null | RespValue[];
17
+ export declare class RedisClient {
18
+ private socket;
19
+ private readonly host;
20
+ private readonly port;
21
+ private readonly username;
22
+ private readonly password;
23
+ private readonly database;
24
+ private readonly useTls;
25
+ private pendingCallbacks;
26
+ private parser;
27
+ constructor(credentials: RedisCredentials);
28
+ /** Establish connection and authenticate. */
29
+ connect(): Promise<void>;
30
+ /** Close the socket gracefully. */
31
+ disconnect(): void;
32
+ rpush(key: string, value: string): Promise<number>;
33
+ llen(key: string): Promise<number>;
34
+ lindex(key: string, index: number): Promise<string | null>;
35
+ lrange(key: string, start: number, stop: number): Promise<string[]>;
36
+ del(...keys: string[]): Promise<number>;
37
+ expire(key: string, seconds: number): Promise<number>;
38
+ /**
39
+ * SET with optional NX and EX flags.
40
+ * Returns 'OK' on success, null if NX condition was not met.
41
+ */
42
+ set(key: string, value: string, options?: {
43
+ nx?: boolean;
44
+ ex?: number;
45
+ }): Promise<string | null>;
46
+ get(key: string): Promise<string | null>;
47
+ /**
48
+ * Execute a Lua script atomically.
49
+ * @param script Lua source code
50
+ * @param keys KEYS array
51
+ * @param args ARGV array
52
+ */
53
+ eval(script: string, keys: string[], args: string[]): Promise<RespValue>;
54
+ private openSocket;
55
+ private send;
56
+ private drainParser;
57
+ }
58
+ export {};
59
+ //# sourceMappingURL=RedisClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RedisClient.d.ts","sourceRoot":"","sources":["../../../../nodes/MessageDebounce/utils/RedisClient.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,KAAK,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,CAAC;AA+EtD,qBAAa,WAAW;IACpB,OAAO,CAAC,MAAM,CAA2B;IACzC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAU;IAEjC,OAAO,CAAC,gBAAgB,CACjB;IACP,OAAO,CAAC,MAAM,CAAoB;gBAEtB,WAAW,EAAE,gBAAgB;IASzC,6CAA6C;IACvC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAuC9B,mCAAmC;IACnC,UAAU,IAAI,IAAI;IAWZ,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIlD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIlC,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAI1D,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAKnE,GAAG,CAAC,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAIvC,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAI3D;;;OAGG;IACG,GAAG,CACL,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,OAAO,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,GACxC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAWnB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAI9C;;;;;OAKG;IACG,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,SAAS,CAAC;IAQ9E,OAAO,CAAC,UAAU;IAgClB,OAAO,CAAC,IAAI;IAWZ,OAAO,CAAC,WAAW;CAetB"}