@orgloop/transform-dedup 0.1.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.
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OrgLoop contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # @orgloop/transform-dedup
2
+
3
+ Deduplicates events within a configurable time window. Events with the same key hash seen within the window are dropped; the first occurrence passes through.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @orgloop/transform-dedup
9
+ ```
10
+
11
+ ## Configuration
12
+
13
+ ```yaml
14
+ transforms:
15
+ - name: dedup-5m
16
+ type: package
17
+ package: "@orgloop/transform-dedup"
18
+ config:
19
+ key: # fields used to build the dedup hash
20
+ - source
21
+ - type
22
+ - provenance.platform_event
23
+ - payload.pr_number
24
+ window: "5m" # time window for dedup (default: 5m)
25
+ store: "memory" # storage backend (only "memory" for now)
26
+ ```
27
+
28
+ ### Config options
29
+
30
+ | Field | Type | Required | Default | Description |
31
+ |-------|------|----------|---------|-------------|
32
+ | `key` | `string[]` | yes | `["source", "type", "id"]` | Dot-path fields used to build the dedup hash. Values are concatenated and SHA-256 hashed |
33
+ | `window` | `string` | yes | `"5m"` | Duration window. Supported units: `ms`, `s`, `m`, `h`, `d` |
34
+ | `store` | `string` | no | `"memory"` | Storage backend. Only `"memory"` is supported in the current version |
35
+
36
+ ### How it works
37
+
38
+ 1. For each incoming event, the transform extracts values at the configured `key` field paths.
39
+ 2. The values are concatenated (null-separated) and hashed with SHA-256.
40
+ 3. If the hash has been seen within the `window` duration, the event is dropped (returns `null`).
41
+ 4. If the hash is new or expired, the event passes through and the hash is recorded with the current timestamp.
42
+
43
+ A periodic cleanup timer evicts expired entries from the in-memory store.
44
+
45
+ ## Example route
46
+
47
+ ```yaml
48
+ routes:
49
+ - name: deduped-pr-reviews
50
+ when:
51
+ source: github-eng
52
+ events:
53
+ - resource.changed
54
+ transforms:
55
+ - ref: dedup-5m
56
+ - ref: humans-only
57
+ then:
58
+ actor: openclaw-agent
59
+ ```
60
+
61
+ ## Auth / prerequisites
62
+
63
+ None.
64
+
65
+ ## Limitations / known issues
66
+
67
+ - **Memory-only store** -- Dedup state is held entirely in memory and is lost on engine restart. After a restart, previously seen events may be processed again until the window catches up.
68
+ - **No distributed dedup** -- The in-memory store does not support multiple engine instances. Running multiple instances results in each instance maintaining its own independent dedup state.
69
+ - **Hash collisions** -- SHA-256 collisions are theoretically possible but practically negligible.
70
+ - **Cleanup interval** -- Expired entries are cleaned up on a timer interval equal to the dedup window (minimum 10 seconds). Between cleanups, the memory footprint grows proportionally to event throughput.
71
+ - **Key field ordering matters** -- The hash is built from key fields in the order specified. Changing the `key` array order produces different hashes.
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Dedup transform — deduplicates events within a configurable time window.
3
+ *
4
+ * Builds a hash from specified key fields and drops events that have
5
+ * been seen within the configured window.
6
+ */
7
+ import type { OrgLoopEvent, Transform, TransformContext } from '@orgloop/sdk';
8
+ export declare class DedupTransform implements Transform {
9
+ readonly id = "dedup";
10
+ private config;
11
+ private windowMs;
12
+ private seen;
13
+ private cleanupTimer;
14
+ init(config: Record<string, unknown>): Promise<void>;
15
+ execute(event: OrgLoopEvent, _context: TransformContext): Promise<OrgLoopEvent | null>;
16
+ shutdown(): Promise<void>;
17
+ private buildHash;
18
+ private cleanup;
19
+ }
20
+ //# sourceMappingURL=dedup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dedup.d.ts","sourceRoot":"","sources":["../src/dedup.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAwB9E,qBAAa,cAAe,YAAW,SAAS;IAC/C,QAAQ,CAAC,EAAE,WAAW;IACtB,OAAO,CAAC,MAAM,CAA0C;IACxD,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,IAAI,CAA6B;IACzC,OAAO,CAAC,YAAY,CAA+C;IAE7D,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBpD,OAAO,CAAC,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,gBAAgB,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAetF,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ/B,OAAO,CAAC,SAAS;IAajB,OAAO,CAAC,OAAO;CAQf"}
package/dist/dedup.js ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Dedup transform — deduplicates events within a configurable time window.
3
+ *
4
+ * Builds a hash from specified key fields and drops events that have
5
+ * been seen within the configured window.
6
+ */
7
+ import { createHash } from 'node:crypto';
8
+ import { parseDuration } from '@orgloop/sdk';
9
+ /**
10
+ * Get a value from a nested object using a dot-separated path.
11
+ */
12
+ function getByPath(obj, path) {
13
+ const segments = path.split('.');
14
+ let current = obj;
15
+ for (const segment of segments) {
16
+ if (current === null || current === undefined || typeof current !== 'object') {
17
+ return undefined;
18
+ }
19
+ current = current[segment];
20
+ }
21
+ return current;
22
+ }
23
+ export class DedupTransform {
24
+ id = 'dedup';
25
+ config = { key: [], window: '5m' };
26
+ windowMs = 5 * 60 * 1000;
27
+ seen = new Map();
28
+ cleanupTimer = null;
29
+ async init(config) {
30
+ const c = config;
31
+ this.config = {
32
+ key: c.key ?? ['source', 'type', 'id'],
33
+ window: c.window ?? '5m',
34
+ };
35
+ this.windowMs = parseDuration(this.config.window);
36
+ // Clean up expired entries every window period (minimum 10s)
37
+ const cleanupInterval = Math.max(this.windowMs, 10_000);
38
+ this.cleanupTimer = setInterval(() => this.cleanup(), cleanupInterval);
39
+ // Allow the process to exit even if cleanup timer is pending
40
+ if (this.cleanupTimer.unref) {
41
+ this.cleanupTimer.unref();
42
+ }
43
+ }
44
+ async execute(event, _context) {
45
+ const hash = this.buildHash(event);
46
+ const now = Date.now();
47
+ const lastSeen = this.seen.get(hash);
48
+ if (lastSeen !== undefined && now - lastSeen < this.windowMs) {
49
+ // Duplicate within window — drop
50
+ return null;
51
+ }
52
+ // New or expired — pass and record
53
+ this.seen.set(hash, now);
54
+ return event;
55
+ }
56
+ async shutdown() {
57
+ if (this.cleanupTimer) {
58
+ clearInterval(this.cleanupTimer);
59
+ this.cleanupTimer = null;
60
+ }
61
+ this.seen.clear();
62
+ }
63
+ buildHash(event) {
64
+ const eventObj = event;
65
+ const parts = [];
66
+ for (const keyPath of this.config.key) {
67
+ const value = getByPath(eventObj, keyPath);
68
+ parts.push(String(value ?? ''));
69
+ }
70
+ const joined = parts.join('\0');
71
+ return createHash('sha256').update(joined).digest('hex');
72
+ }
73
+ cleanup() {
74
+ const now = Date.now();
75
+ for (const [hash, timestamp] of this.seen) {
76
+ if (now - timestamp >= this.windowMs) {
77
+ this.seen.delete(hash);
78
+ }
79
+ }
80
+ }
81
+ }
82
+ //# sourceMappingURL=dedup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dedup.js","sourceRoot":"","sources":["../src/dedup.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @orgloop/transform-dedup — registration entry point.
3
+ */
4
+ import type { TransformRegistration } from '@orgloop/sdk';
5
+ export declare function register(): TransformRegistration;
6
+ export { DedupTransform } from './dedup.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAG1D,wBAAgB,QAAQ,IAAI,qBAAqB,CA4BhD;AAED,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @orgloop/transform-dedup — registration entry point.
3
+ */
4
+ import { DedupTransform } from './dedup.js';
5
+ export function register() {
6
+ return {
7
+ id: 'dedup',
8
+ transform: DedupTransform,
9
+ configSchema: {
10
+ type: 'object',
11
+ properties: {
12
+ key: {
13
+ type: 'array',
14
+ items: { type: 'string' },
15
+ description: 'Event field paths to use as dedup key.',
16
+ },
17
+ window: {
18
+ type: 'string',
19
+ description: 'Duration window for deduplication (e.g., "5m").',
20
+ default: '5m',
21
+ },
22
+ store: {
23
+ type: 'string',
24
+ enum: ['memory'],
25
+ description: 'Storage backend (only "memory" for MVP).',
26
+ default: 'memory',
27
+ },
28
+ },
29
+ required: ['key', 'window'],
30
+ additionalProperties: false,
31
+ },
32
+ };
33
+ }
34
+ export { DedupTransform } from './dedup.js';
35
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE5C,MAAM,UAAU,QAAQ;IACvB,OAAO;QACN,EAAE,EAAE,OAAO;QACX,SAAS,EAAE,cAAc;QACzB,YAAY,EAAE;YACb,IAAI,EAAE,QAAQ;YACd,UAAU,EAAE;gBACX,GAAG,EAAE;oBACJ,IAAI,EAAE,OAAO;oBACb,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;oBACzB,WAAW,EAAE,wCAAwC;iBACrD;gBACD,MAAM,EAAE;oBACP,IAAI,EAAE,QAAQ;oBACd,WAAW,EAAE,iDAAiD;oBAC9D,OAAO,EAAE,IAAI;iBACb;gBACD,KAAK,EAAE;oBACN,IAAI,EAAE,QAAQ;oBACd,IAAI,EAAE,CAAC,QAAQ,CAAC;oBAChB,WAAW,EAAE,0CAA0C;oBACvD,OAAO,EAAE,QAAQ;iBACjB;aACD;YACD,QAAQ,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC;YAC3B,oBAAoB,EAAE,KAAK;SAC3B;KACD,CAAC;AACH,CAAC;AAED,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC"}
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@orgloop/transform-dedup",
3
+ "version": "0.1.0",
4
+ "description": "OrgLoop dedup transform — deduplicate events within a time window",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "dependencies": {
9
+ "@orgloop/sdk": "0.1.0"
10
+ },
11
+ "orgloop": {
12
+ "type": "transform",
13
+ "id": "dedup"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "license": "MIT",
22
+ "scripts": {
23
+ "build": "tsc",
24
+ "clean": "rm -rf dist",
25
+ "typecheck": "tsc --noEmit",
26
+ "test": "vitest run"
27
+ }
28
+ }