@bernierllc/nevar-loop-controller 0.0.1 → 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/README.md CHANGED
@@ -1,45 +1,86 @@
1
1
  # @bernierllc/nevar-loop-controller
2
2
 
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
3
+ Runtime loop lifecycle management for the Nevar rules engine. Controls opt-in infinite loop execution with heartbeat monitoring and concurrency limits.
4
4
 
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
5
+ ## Installation
6
6
 
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
7
+ ```bash
8
+ npm install @bernierllc/nevar-loop-controller
9
+ ```
8
10
 
9
- ## Purpose
11
+ ## Usage
10
12
 
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `@bernierllc/nevar-loop-controller`
13
- 2. Enable secure, token-less publishing from CI/CD workflows
14
- 3. Establish provenance for packages published under this name
13
+ ```typescript
14
+ import { LoopController } from '@bernierllc/nevar-loop-controller';
15
15
 
16
- ## What is OIDC Trusted Publishing?
16
+ const controller = new LoopController({
17
+ allowInfiniteLoops: true,
18
+ maxConcurrentLoops: 5,
19
+ heartbeatIntervalMs: 5000,
20
+ maxMissedHeartbeats: 3,
21
+ });
17
22
 
18
- OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
23
+ // Check if loops are allowed
24
+ if (controller.isAllowed()) {
25
+ const handle = controller.startLoop('order.monitor');
19
26
 
20
- ## Setup Instructions
27
+ if (handle) {
28
+ // Send heartbeats during execution
29
+ controller.heartbeat(handle.id);
21
30
 
22
- To properly configure OIDC trusted publishing for this package:
31
+ // Inspect loop state
32
+ const state = controller.inspect(handle.id);
23
33
 
24
- 1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
25
- 2. Configure the trusted publisher (e.g., GitHub Actions)
26
- 3. Specify the repository and workflow that should be allowed to publish
27
- 4. Use the configured workflow to publish your actual package
34
+ // Pause/resume
35
+ controller.pause(handle.id);
36
+ controller.resume(handle.id);
28
37
 
29
- ## DO NOT USE THIS PACKAGE
38
+ // Complete when done
39
+ controller.complete(handle.id);
40
+ }
41
+ }
30
42
 
31
- This package is a placeholder for OIDC configuration only. It:
32
- - Contains no executable code
33
- - Provides no functionality
34
- - Should not be installed as a dependency
35
- - Exists only for administrative purposes
43
+ // Kill stale loops that missed heartbeats
44
+ const killedIds = controller.checkHeartbeats();
36
45
 
37
- ## More Information
46
+ // List all active loops
47
+ const active = controller.list();
48
+ ```
38
49
 
39
- For more details about npm's trusted publishing feature, see:
40
- - [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
41
- - [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
50
+ ## API
42
51
 
43
- ---
52
+ ### `LoopController`
44
53
 
45
- **Maintained for OIDC setup purposes only**
54
+ Manages the lifecycle of opt-in infinite loops with heartbeat-based health monitoring.
55
+
56
+ **Constructor:** `new LoopController(config?: Partial<LoopConfig>)`
57
+
58
+ Default config: `allowInfiniteLoops: false`, `maxConcurrentLoops: 0`, `heartbeatIntervalMs: 5000`, `maxMissedHeartbeats: 3`
59
+
60
+ - `isAllowed()` - Returns `true` if loops are enabled and concurrency limit is greater than zero
61
+ - `startLoop(triggerKey)` - Creates a new loop handle. Returns `null` if loops are not allowed or at concurrency limit
62
+ - `heartbeat(loopId)` - Records a heartbeat and increments iteration count. Returns `false` if loop not found or not running
63
+ - `kill(loopId)` - Forcefully terminates a loop
64
+ - `pause(loopId)` - Pauses a running loop
65
+ - `resume(loopId)` - Resumes a paused loop
66
+ - `complete(loopId)` - Marks a loop as completed
67
+ - `list()` - Returns all active (running or paused) loop handles
68
+ - `inspect(loopId)` - Returns the full `LoopHandle` for a given loop, or `null`
69
+ - `checkHeartbeats()` - Kills loops that have missed too many heartbeats. Returns array of killed loop IDs
70
+ - `getConfig()` - Returns a copy of the current configuration
71
+
72
+ ### `LoopControllerError`
73
+
74
+ Error class for loop controller failures. Extends `Error` with `code` and `context` properties.
75
+
76
+ ## Integration Documentation
77
+
78
+ ### Logger Integration
79
+ This package does not integrate with `@bernierllc/logger`. As a core package, logger integration is optional and not included by default. Consumers should handle logging at the service layer.
80
+
81
+ ### NeverHub Integration
82
+ This package does not integrate with `@bernierllc/neverhub-adapter`. As a core package, NeverHub integration is not applicable. NeverHub registration should be handled by service-layer packages that compose this package.
83
+
84
+ ## License
85
+
86
+ Copyright (c) 2025 Bernier LLC. All rights reserved.
@@ -0,0 +1 @@
1
+ export { NevarLoopError as LoopControllerError } from '@bernierllc/nevar-types';
package/dist/errors.js ADDED
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ /*
3
+ Copyright (c) 2025 Bernier LLC
4
+
5
+ This file is licensed to the client under a limited-use license.
6
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
7
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.LoopControllerError = void 0;
11
+ var nevar_types_1 = require("@bernierllc/nevar-types");
12
+ Object.defineProperty(exports, "LoopControllerError", { enumerable: true, get: function () { return nevar_types_1.NevarLoopError; } });
@@ -0,0 +1,2 @@
1
+ export { LoopController } from './loop-controller';
2
+ export { LoopControllerError } from './errors';
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ /*
3
+ Copyright (c) 2025 Bernier LLC
4
+
5
+ This file is licensed to the client under a limited-use license.
6
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
7
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.LoopControllerError = exports.LoopController = void 0;
11
+ var loop_controller_1 = require("./loop-controller");
12
+ Object.defineProperty(exports, "LoopController", { enumerable: true, get: function () { return loop_controller_1.LoopController; } });
13
+ var errors_1 = require("./errors");
14
+ Object.defineProperty(exports, "LoopControllerError", { enumerable: true, get: function () { return errors_1.LoopControllerError; } });
@@ -0,0 +1,18 @@
1
+ import type { LoopConfig, LoopHandle } from '@bernierllc/nevar-types';
2
+ export declare class LoopController {
3
+ private readonly config;
4
+ private readonly loops;
5
+ constructor(config?: Partial<LoopConfig>);
6
+ isAllowed(): boolean;
7
+ startLoop(triggerKey: string): LoopHandle | null;
8
+ heartbeat(loopId: string): boolean;
9
+ kill(loopId: string): boolean;
10
+ pause(loopId: string): boolean;
11
+ resume(loopId: string): boolean;
12
+ complete(loopId: string): boolean;
13
+ list(): LoopHandle[];
14
+ inspect(loopId: string): LoopHandle | null;
15
+ checkHeartbeats(): string[];
16
+ getConfig(): LoopConfig;
17
+ private getActiveLoops;
18
+ }
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ /*
3
+ Copyright (c) 2025 Bernier LLC
4
+
5
+ This file is licensed to the client under a limited-use license.
6
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
7
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.LoopController = void 0;
11
+ const crypto_1 = require("crypto");
12
+ const DEFAULT_CONFIG = {
13
+ allowInfiniteLoops: false,
14
+ maxConcurrentLoops: 0,
15
+ heartbeatIntervalMs: 5000,
16
+ maxMissedHeartbeats: 3,
17
+ };
18
+ class LoopController {
19
+ config;
20
+ loops = new Map();
21
+ constructor(config) {
22
+ this.config = { ...DEFAULT_CONFIG, ...config };
23
+ }
24
+ isAllowed() {
25
+ return !!this.config.allowInfiniteLoops && (this.config.maxConcurrentLoops ?? 0) > 0;
26
+ }
27
+ startLoop(triggerKey) {
28
+ if (!this.isAllowed()) {
29
+ return null;
30
+ }
31
+ const activeCount = this.getActiveLoops().length;
32
+ if (activeCount >= this.config.maxConcurrentLoops) {
33
+ return null;
34
+ }
35
+ const now = new Date();
36
+ const handle = {
37
+ id: (0, crypto_1.randomUUID)(),
38
+ triggerKey,
39
+ startedAt: now,
40
+ iterations: 0,
41
+ status: 'running',
42
+ lastHeartbeat: now,
43
+ };
44
+ this.loops.set(handle.id, handle);
45
+ return handle;
46
+ }
47
+ heartbeat(loopId) {
48
+ const handle = this.loops.get(loopId);
49
+ if (!handle || handle.status !== 'running') {
50
+ return false;
51
+ }
52
+ handle.lastHeartbeat = new Date();
53
+ handle.iterations = (handle.iterations ?? 0) + 1;
54
+ return true;
55
+ }
56
+ kill(loopId) {
57
+ const handle = this.loops.get(loopId);
58
+ if (!handle) {
59
+ return false;
60
+ }
61
+ handle.status = 'killed';
62
+ return true;
63
+ }
64
+ pause(loopId) {
65
+ const handle = this.loops.get(loopId);
66
+ if (!handle || handle.status !== 'running') {
67
+ return false;
68
+ }
69
+ handle.status = 'paused';
70
+ return true;
71
+ }
72
+ resume(loopId) {
73
+ const handle = this.loops.get(loopId);
74
+ if (!handle || handle.status !== 'paused') {
75
+ return false;
76
+ }
77
+ handle.status = 'running';
78
+ return true;
79
+ }
80
+ complete(loopId) {
81
+ const handle = this.loops.get(loopId);
82
+ if (!handle) {
83
+ return false;
84
+ }
85
+ handle.status = 'completed';
86
+ return true;
87
+ }
88
+ list() {
89
+ return this.getActiveLoops();
90
+ }
91
+ inspect(loopId) {
92
+ return this.loops.get(loopId) ?? null;
93
+ }
94
+ checkHeartbeats() {
95
+ const now = Date.now();
96
+ const threshold = this.config.heartbeatIntervalMs * this.config.maxMissedHeartbeats;
97
+ const killedIds = [];
98
+ for (const handle of this.loops.values()) {
99
+ if (handle.status !== 'running') {
100
+ continue;
101
+ }
102
+ const elapsed = now - handle.lastHeartbeat.getTime();
103
+ if (elapsed > threshold) {
104
+ handle.status = 'killed';
105
+ killedIds.push(handle.id);
106
+ }
107
+ }
108
+ return killedIds;
109
+ }
110
+ getConfig() {
111
+ return { ...this.config };
112
+ }
113
+ getActiveLoops() {
114
+ const active = [];
115
+ for (const handle of this.loops.values()) {
116
+ if (handle.status === 'running' || handle.status === 'paused') {
117
+ active.push(handle);
118
+ }
119
+ }
120
+ return active;
121
+ }
122
+ }
123
+ exports.LoopController = LoopController;
package/package.json CHANGED
@@ -1,10 +1,56 @@
1
1
  {
2
2
  "name": "@bernierllc/nevar-loop-controller",
3
- "version": "0.0.1",
4
- "description": "OIDC trusted publishing setup package for @bernierllc/nevar-loop-controller",
3
+ "version": "0.1.0",
4
+ "description": "Supervised loop management with heartbeat monitoring for the Nevar rules engine",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist/**/*",
9
+ "README.md",
10
+ "LICENSE"
11
+ ],
5
12
  "keywords": [
6
- "oidc",
7
- "trusted-publishing",
8
- "setup"
9
- ]
10
- }
13
+ "nevar",
14
+ "rules-engine",
15
+ "loop-controller",
16
+ "heartbeat",
17
+ "supervised-loops"
18
+ ],
19
+ "author": "Bernier LLC",
20
+ "license": "SEE LICENSE IN LICENSE",
21
+ "publishConfig": {
22
+ "access": "public",
23
+ "registry": "https://registry.npmjs.org/"
24
+ },
25
+ "engines": {
26
+ "node": ">=16.0.0"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/bernierllc/tools.git",
31
+ "directory": "packages/core/nevar-loop-controller"
32
+ },
33
+ "dependencies": {
34
+ "@bernierllc/nevar-types": "0.1.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/jest": "^29.5.0",
38
+ "@types/node": "^20.0.0",
39
+ "@typescript-eslint/eslint-plugin": "^6.0.0",
40
+ "@typescript-eslint/parser": "^6.0.0",
41
+ "eslint": "^8.0.0",
42
+ "jest": "^29.5.0",
43
+ "rimraf": "^5.0.0",
44
+ "ts-jest": "^29.1.0",
45
+ "typescript": "^5.0.0"
46
+ },
47
+ "scripts": {
48
+ "build": "tsc",
49
+ "prebuild": "npm run clean",
50
+ "clean": "rimraf dist",
51
+ "test": "jest",
52
+ "test:run": "jest",
53
+ "test:coverage": "jest --coverage",
54
+ "lint": "eslint src __tests__ --ext .ts"
55
+ }
56
+ }