pepr 0.0.0-development

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 (58) hide show
  1. package/.prettierignore +1 -0
  2. package/CODE_OF_CONDUCT.md +133 -0
  3. package/LICENSE +201 -0
  4. package/README.md +151 -0
  5. package/SECURITY.md +18 -0
  6. package/SUPPORT.md +16 -0
  7. package/codecov.yaml +19 -0
  8. package/commitlint.config.js +1 -0
  9. package/package.json +70 -0
  10. package/src/cli.ts +48 -0
  11. package/src/lib/assets/deploy.ts +122 -0
  12. package/src/lib/assets/destroy.ts +33 -0
  13. package/src/lib/assets/helm.ts +219 -0
  14. package/src/lib/assets/index.ts +175 -0
  15. package/src/lib/assets/loader.ts +41 -0
  16. package/src/lib/assets/networking.ts +89 -0
  17. package/src/lib/assets/pods.ts +353 -0
  18. package/src/lib/assets/rbac.ts +111 -0
  19. package/src/lib/assets/store.ts +49 -0
  20. package/src/lib/assets/webhooks.ts +147 -0
  21. package/src/lib/assets/yaml.ts +234 -0
  22. package/src/lib/capability.ts +314 -0
  23. package/src/lib/controller/index.ts +326 -0
  24. package/src/lib/controller/store.ts +219 -0
  25. package/src/lib/errors.ts +20 -0
  26. package/src/lib/filter.ts +110 -0
  27. package/src/lib/helpers.ts +342 -0
  28. package/src/lib/included-files.ts +19 -0
  29. package/src/lib/k8s.ts +169 -0
  30. package/src/lib/logger.ts +27 -0
  31. package/src/lib/metrics.ts +120 -0
  32. package/src/lib/module.ts +136 -0
  33. package/src/lib/mutate-processor.ts +160 -0
  34. package/src/lib/mutate-request.ts +153 -0
  35. package/src/lib/queue.ts +89 -0
  36. package/src/lib/schedule.ts +175 -0
  37. package/src/lib/storage.ts +192 -0
  38. package/src/lib/tls.ts +90 -0
  39. package/src/lib/types.ts +215 -0
  40. package/src/lib/utils.ts +57 -0
  41. package/src/lib/validate-processor.ts +80 -0
  42. package/src/lib/validate-request.ts +102 -0
  43. package/src/lib/watch-processor.ts +124 -0
  44. package/src/lib.ts +27 -0
  45. package/src/runtime/controller.ts +75 -0
  46. package/src/sdk/sdk.ts +116 -0
  47. package/src/templates/.eslintrc.template.json +18 -0
  48. package/src/templates/.prettierrc.json +13 -0
  49. package/src/templates/README.md +21 -0
  50. package/src/templates/capabilities/hello-pepr.samples.json +160 -0
  51. package/src/templates/capabilities/hello-pepr.ts +426 -0
  52. package/src/templates/gitignore +4 -0
  53. package/src/templates/package.json +20 -0
  54. package/src/templates/pepr.code-snippets.json +21 -0
  55. package/src/templates/pepr.ts +17 -0
  56. package/src/templates/settings.json +10 -0
  57. package/src/templates/tsconfig.json +9 -0
  58. package/src/templates/tsconfig.module.json +19 -0
@@ -0,0 +1,136 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+ import { clone } from "ramda";
4
+ import { Capability } from "./capability";
5
+ import { Controller } from "./controller";
6
+ import { ValidateError } from "./errors";
7
+ import { AdmissionRequest, MutateResponse, ValidateResponse, WebhookIgnore } from "./k8s";
8
+ import { CapabilityExport } from "./types";
9
+ import { setupWatch } from "./watch-processor";
10
+ import { Log } from "../lib";
11
+
12
+ /** Custom Labels Type for package.json */
13
+ export interface CustomLabels {
14
+ namespace?: Record<string, string>;
15
+ }
16
+ /** Global configuration for the Pepr runtime. */
17
+ export type ModuleConfig = {
18
+ /** The Pepr version this module uses */
19
+ peprVersion?: string;
20
+ /** The user-defined version of the module */
21
+ appVersion?: string;
22
+ /** A unique identifier for this Pepr module. This is automatically generated by Pepr. */
23
+ uuid: string;
24
+ /** A description of the Pepr module and what it does. */
25
+ description?: string;
26
+ /** The webhookTimeout */
27
+ webhookTimeout?: number;
28
+ /** Reject K8s resource AdmissionRequests on error. */
29
+ onError?: string;
30
+ /** Configure global exclusions that will never be processed by Pepr. */
31
+ alwaysIgnore: WebhookIgnore;
32
+ /** Define the log level for the in-cluster controllers */
33
+ logLevel?: string;
34
+ /** Propagate env variables to in-cluster controllers */
35
+ env?: Record<string, string>;
36
+ /** Custom Labels for Kubernetes Objects */
37
+ customLabels?: CustomLabels;
38
+ };
39
+
40
+ export type PackageJSON = {
41
+ description: string;
42
+ pepr: ModuleConfig;
43
+ };
44
+
45
+ export type PeprModuleOptions = {
46
+ deferStart?: boolean;
47
+
48
+ /** A user-defined callback to pre-process or intercept a Pepr request from K8s immediately before it is processed */
49
+ beforeHook?: (req: AdmissionRequest) => void;
50
+
51
+ /** A user-defined callback to post-process or intercept a Pepr response just before it is returned to K8s */
52
+ afterHook?: (res: MutateResponse | ValidateResponse) => void;
53
+ };
54
+
55
+ // Track if this is a watch mode controller
56
+ export const isWatchMode = () => process.env.PEPR_WATCH_MODE === "true";
57
+
58
+ // Track if Pepr is running in build mode
59
+ export const isBuildMode = () => process.env.PEPR_MODE === "build";
60
+
61
+ export const isDevMode = () => process.env.PEPR_MODE === "dev";
62
+
63
+ export class PeprModule {
64
+ #controller!: Controller;
65
+
66
+ /**
67
+ * Create a new Pepr runtime
68
+ *
69
+ * @param config The configuration for the Pepr runtime
70
+ * @param capabilities The capabilities to be loaded into the Pepr runtime
71
+ * @param opts Options for the Pepr runtime
72
+ */
73
+ constructor({ description, pepr }: PackageJSON, capabilities: Capability[] = [], opts: PeprModuleOptions = {}) {
74
+ const config: ModuleConfig = clone(pepr);
75
+ config.description = description;
76
+
77
+ // Need to validate at runtime since TS gets sad about parsing the package.json
78
+ ValidateError(config.onError);
79
+
80
+ // Handle build mode
81
+ if (isBuildMode()) {
82
+ // Fail if process.send is not defined
83
+ if (!process.send) {
84
+ throw new Error("process.send is not defined");
85
+ }
86
+
87
+ const exportedCapabilities: CapabilityExport[] = [];
88
+
89
+ // Send capability map to parent process
90
+ for (const capability of capabilities) {
91
+ // Convert the capability to a capability config
92
+ exportedCapabilities.push({
93
+ name: capability.name,
94
+ description: capability.description,
95
+ namespaces: capability.namespaces,
96
+ bindings: capability.bindings,
97
+ hasSchedule: capability.hasSchedule,
98
+ });
99
+ }
100
+
101
+ // Send the capabilities back to the parent process
102
+ process.send(exportedCapabilities);
103
+
104
+ return;
105
+ }
106
+
107
+ this.#controller = new Controller(config, capabilities, opts.beforeHook, opts.afterHook, () => {
108
+ // Wait for the controller to be ready before setting up watches
109
+ if (isWatchMode() || isDevMode()) {
110
+ try {
111
+ setupWatch(capabilities);
112
+ } catch (e) {
113
+ Log.error(e, "Error setting up watch");
114
+ process.exit(1);
115
+ }
116
+ }
117
+ });
118
+
119
+ // Stop processing if deferStart is set to true
120
+ if (opts.deferStart) {
121
+ return;
122
+ }
123
+
124
+ this.start();
125
+ }
126
+
127
+ /**
128
+ * Start the Pepr runtime manually.
129
+ * Normally this is called automatically when the Pepr module is instantiated, but can be called manually if `deferStart` is set to `true` in the constructor.
130
+ *
131
+ * @param port
132
+ */
133
+ start = (port = 3000) => {
134
+ this.#controller.startServer(port);
135
+ };
136
+ }
@@ -0,0 +1,160 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import jsonPatch from "fast-json-patch";
5
+ import { kind } from "kubernetes-fluent-client";
6
+
7
+ import { Capability } from "./capability";
8
+ import { Errors } from "./errors";
9
+ import { shouldSkipRequest } from "./filter";
10
+ import { MutateResponse, AdmissionRequest } from "./k8s";
11
+ import Log from "./logger";
12
+ import { ModuleConfig } from "./module";
13
+ import { PeprMutateRequest } from "./mutate-request";
14
+ import { base64Encode, convertFromBase64Map, convertToBase64Map } from "./utils";
15
+
16
+ export async function mutateProcessor(
17
+ config: ModuleConfig,
18
+ capabilities: Capability[],
19
+ req: AdmissionRequest,
20
+ reqMetadata: Record<string, string>,
21
+ ): Promise<MutateResponse> {
22
+ const wrapped = new PeprMutateRequest(req);
23
+ const response: MutateResponse = {
24
+ uid: req.uid,
25
+ warnings: [],
26
+ allowed: false,
27
+ };
28
+
29
+ // Track whether any capability matched the request
30
+ let matchedAction = false;
31
+
32
+ // Track data fields that should be skipped during decoding
33
+ let skipDecode: string[] = [];
34
+
35
+ // If the resource is a secret, decode the data
36
+ const isSecret = req.kind.version == "v1" && req.kind.kind == "Secret";
37
+ if (isSecret) {
38
+ skipDecode = convertFromBase64Map(wrapped.Raw as unknown as kind.Secret);
39
+ }
40
+
41
+ Log.info(reqMetadata, `Processing request`);
42
+
43
+ for (const { name, bindings, namespaces } of capabilities) {
44
+ const actionMetadata = { ...reqMetadata, name };
45
+
46
+ for (const action of bindings) {
47
+ // Skip this action if it's not a mutate action
48
+ if (!action.mutateCallback) {
49
+ continue;
50
+ }
51
+
52
+ // Continue to the next action without doing anything if this one should be skipped
53
+ if (shouldSkipRequest(action, req, namespaces)) {
54
+ continue;
55
+ }
56
+
57
+ const label = action.mutateCallback.name;
58
+ Log.info(actionMetadata, `Processing mutation action (${label})`);
59
+
60
+ matchedAction = true;
61
+
62
+ // Add annotations to the request to indicate that the capability started processing
63
+ // this will allow tracking of failed mutations that were permitted to continue
64
+ const updateStatus = (status: string) => {
65
+ // Only update the status if the request is a CREATE or UPDATE (we don't use CONNECT)
66
+ if (req.operation == "DELETE") {
67
+ return;
68
+ }
69
+
70
+ const identifier = `${config.uuid}.pepr.dev/${name}`;
71
+ wrapped.Raw.metadata = wrapped.Raw.metadata || {};
72
+ wrapped.Raw.metadata.annotations = wrapped.Raw.metadata.annotations || {};
73
+ wrapped.Raw.metadata.annotations[identifier] = status;
74
+ };
75
+
76
+ updateStatus("started");
77
+
78
+ try {
79
+ // Run the action
80
+ await action.mutateCallback(wrapped);
81
+
82
+ Log.info(actionMetadata, `Mutation action succeeded (${label})`);
83
+
84
+ // Add annotations to the request to indicate that the capability succeeded
85
+ updateStatus("succeeded");
86
+ } catch (e) {
87
+ updateStatus("warning");
88
+ response.warnings = response.warnings || [];
89
+
90
+ let errorMessage = "";
91
+
92
+ try {
93
+ if (e.message && e.message !== "[object Object]") {
94
+ errorMessage = e.message;
95
+ } else {
96
+ throw new Error("An error occurred in the mutate action.");
97
+ }
98
+ } catch (e) {
99
+ errorMessage = "An error occurred with the mutate action.";
100
+ }
101
+
102
+ Log.error(actionMetadata, `Action failed: ${errorMessage}`);
103
+ response.warnings.push(`Action failed: ${errorMessage}`);
104
+
105
+ switch (config.onError) {
106
+ case Errors.reject:
107
+ Log.error(actionMetadata, `Action failed: ${errorMessage}`);
108
+ response.result = "Pepr module configured to reject on error";
109
+ return response;
110
+
111
+ case Errors.audit:
112
+ response.auditAnnotations = response.auditAnnotations || {};
113
+ response.auditAnnotations[Date.now()] = `Action failed: ${errorMessage}`;
114
+ break;
115
+ }
116
+ }
117
+ }
118
+ }
119
+
120
+ // If we've made it this far, the request is allowed
121
+ response.allowed = true;
122
+
123
+ // If no capability matched the request, exit early
124
+ if (!matchedAction) {
125
+ Log.info(reqMetadata, `No matching actions found`);
126
+ return response;
127
+ }
128
+
129
+ // delete operations can't be mutate, just return before the transformation
130
+ if (req.operation == "DELETE") {
131
+ return response;
132
+ }
133
+
134
+ const transformed = wrapped.Raw;
135
+
136
+ // Post-process the Secret requests to convert it back to the original format
137
+ if (isSecret) {
138
+ convertToBase64Map(transformed as unknown as kind.Secret, skipDecode);
139
+ }
140
+
141
+ // Compare the original request to the modified request to get the patches
142
+ const patches = jsonPatch.compare(req.object, transformed);
143
+
144
+ // Only add the patch if there are patches to apply
145
+ if (patches.length > 0) {
146
+ response.patchType = "JSONPatch";
147
+ // Webhook must be base64-encoded
148
+ // https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#response
149
+ response.patch = base64Encode(JSON.stringify(patches));
150
+ }
151
+
152
+ // Remove the warnings array if it's empty
153
+ if (response.warnings && response.warnings.length < 1) {
154
+ delete response.warnings;
155
+ }
156
+
157
+ Log.debug({ ...reqMetadata, patches }, `Patches generated`);
158
+
159
+ return response;
160
+ }
@@ -0,0 +1,153 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import { KubernetesObject } from "kubernetes-fluent-client";
5
+ import { clone, mergeDeepRight } from "ramda";
6
+
7
+ import { AdmissionRequest, Operation } from "./k8s";
8
+ import { DeepPartial } from "./types";
9
+
10
+ /**
11
+ * The RequestWrapper class provides methods to modify Kubernetes objects in the context
12
+ * of a mutating webhook request.
13
+ */
14
+ export class PeprMutateRequest<T extends KubernetesObject> {
15
+ Raw: T;
16
+
17
+ #input: AdmissionRequest<T>;
18
+
19
+ get PermitSideEffects() {
20
+ return !this.#input.dryRun;
21
+ }
22
+
23
+ /**
24
+ * Indicates whether the request is a dry run.
25
+ * @returns true if the request is a dry run, false otherwise.
26
+ */
27
+ get IsDryRun() {
28
+ return this.#input.dryRun;
29
+ }
30
+
31
+ /**
32
+ * Provides access to the old resource in the request if available.
33
+ * @returns The old Kubernetes resource object or null if not available.
34
+ */
35
+ get OldResource() {
36
+ return this.#input.oldObject;
37
+ }
38
+
39
+ /**
40
+ * Provides access to the request object.
41
+ * @returns The request object containing the Kubernetes resource.
42
+ */
43
+ get Request() {
44
+ return this.#input;
45
+ }
46
+
47
+ /**
48
+ * Creates a new instance of the action class.
49
+ * @param input - The request object containing the Kubernetes resource to modify.
50
+ */
51
+ constructor(input: AdmissionRequest<T>) {
52
+ this.#input = input;
53
+
54
+ // If this is a DELETE operation, use the oldObject instead
55
+ if (input.operation.toUpperCase() === Operation.DELETE) {
56
+ this.Raw = clone(input.oldObject as T);
57
+ } else {
58
+ // Otherwise, use the incoming object
59
+ this.Raw = clone(input.object);
60
+ }
61
+
62
+ if (!this.Raw) {
63
+ throw new Error("unable to load the request object into PeprRequest.RawP");
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Deep merges the provided object with the current resource.
69
+ *
70
+ * @param obj - The object to merge with the current resource.
71
+ */
72
+ Merge = (obj: DeepPartial<T>) => {
73
+ this.Raw = mergeDeepRight(this.Raw, obj) as unknown as T;
74
+ };
75
+
76
+ /**
77
+ * Updates a label on the Kubernetes resource.
78
+ * @param key - The key of the label to update.
79
+ * @param value - The value of the label.
80
+ * @returns The current action instance for method chaining.
81
+ */
82
+ SetLabel = (key: string, value: string) => {
83
+ const ref = this.Raw;
84
+
85
+ ref.metadata = ref.metadata ?? {};
86
+ ref.metadata.labels = ref.metadata.labels ?? {};
87
+ ref.metadata.labels[key] = value;
88
+
89
+ return this;
90
+ };
91
+
92
+ /**
93
+ * Updates an annotation on the Kubernetes resource.
94
+ * @param key - The key of the annotation to update.
95
+ * @param value - The value of the annotation.
96
+ * @returns The current action instance for method chaining.
97
+ */
98
+ SetAnnotation = (key: string, value: string) => {
99
+ const ref = this.Raw;
100
+
101
+ ref.metadata = ref.metadata ?? {};
102
+ ref.metadata.annotations = ref.metadata.annotations ?? {};
103
+ ref.metadata.annotations[key] = value;
104
+
105
+ return this;
106
+ };
107
+
108
+ /**
109
+ * Removes a label from the Kubernetes resource.
110
+ * @param key - The key of the label to remove.
111
+ * @returns The current Action instance for method chaining.
112
+ */
113
+ RemoveLabel = (key: string) => {
114
+ if (this.Raw.metadata?.labels?.[key]) {
115
+ delete this.Raw.metadata.labels[key];
116
+ }
117
+
118
+ return this;
119
+ };
120
+
121
+ /**
122
+ * Removes an annotation from the Kubernetes resource.
123
+ * @param key - The key of the annotation to remove.
124
+ * @returns The current Action instance for method chaining.
125
+ */
126
+ RemoveAnnotation = (key: string) => {
127
+ if (this.Raw.metadata?.annotations?.[key]) {
128
+ delete this.Raw.metadata.annotations[key];
129
+ }
130
+
131
+ return this;
132
+ };
133
+
134
+ /**
135
+ * Check if a label exists on the Kubernetes resource.
136
+ *
137
+ * @param key the label key to check
138
+ * @returns
139
+ */
140
+ HasLabel = (key: string) => {
141
+ return this.Raw.metadata?.labels?.[key] !== undefined;
142
+ };
143
+
144
+ /**
145
+ * Check if an annotation exists on the Kubernetes resource.
146
+ *
147
+ * @param key the annotation key to check
148
+ * @returns
149
+ */
150
+ HasAnnotation = (key: string) => {
151
+ return this.Raw.metadata?.annotations?.[key] !== undefined;
152
+ };
153
+ }
@@ -0,0 +1,89 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+ import { KubernetesObject } from "@kubernetes/client-node";
4
+ import { WatchPhase } from "kubernetes-fluent-client/dist/fluent/types";
5
+ import Log from "./logger";
6
+
7
+ type QueueItem<K extends KubernetesObject> = {
8
+ item: K;
9
+ type: WatchPhase;
10
+ resolve: (value: void | PromiseLike<void>) => void;
11
+ reject: (reason?: string) => void;
12
+ };
13
+
14
+ /**
15
+ * Queue is a FIFO queue for reconciling
16
+ */
17
+ export class Queue<K extends KubernetesObject> {
18
+ #queue: QueueItem<K>[] = [];
19
+ #pendingPromise = false;
20
+ #reconcile?: (obj: KubernetesObject, type: WatchPhase) => Promise<void>;
21
+
22
+ constructor() {
23
+ this.#reconcile = async () => await new Promise(resolve => resolve());
24
+ }
25
+
26
+ setReconcile(reconcile: (obj: KubernetesObject, type: WatchPhase) => Promise<void>) {
27
+ this.#reconcile = reconcile;
28
+ }
29
+
30
+ /**
31
+ * Enqueue adds an item to the queue and returns a promise that resolves when the item is
32
+ * reconciled.
33
+ *
34
+ * @param item The object to reconcile
35
+ * @returns A promise that resolves when the object is reconciled
36
+ */
37
+ enqueue(item: K, type: WatchPhase) {
38
+ Log.debug(`Enqueueing ${item.metadata!.namespace}/${item.metadata!.name}`);
39
+ return new Promise<void>((resolve, reject) => {
40
+ this.#queue.push({ item, type, resolve, reject });
41
+ return this.#dequeue();
42
+ });
43
+ }
44
+
45
+ /**
46
+ * Dequeue reconciles the next item in the queue
47
+ *
48
+ * @returns A promise that resolves when the webapp is reconciled
49
+ */
50
+ async #dequeue() {
51
+ // If there is a pending promise, do nothing
52
+ if (this.#pendingPromise) {
53
+ Log.debug("Pending promise, not dequeuing");
54
+ return false;
55
+ }
56
+
57
+ // Take the next element from the queue
58
+ const element = this.#queue.shift();
59
+
60
+ // If there is no element, do nothing
61
+ if (!element) {
62
+ Log.debug("No element, not dequeuing");
63
+ return false;
64
+ }
65
+
66
+ try {
67
+ // Set the pending promise flag to avoid concurrent reconciliations
68
+ this.#pendingPromise = true;
69
+
70
+ // Reconcile the element
71
+ if (this.#reconcile) {
72
+ Log.debug(`Reconciling ${element.item.metadata!.name}`);
73
+ await this.#reconcile(element.item, element.type);
74
+ }
75
+
76
+ element.resolve();
77
+ } catch (e) {
78
+ Log.debug(`Error reconciling ${element.item.metadata!.name}`, { error: e });
79
+ element.reject(e);
80
+ } finally {
81
+ // Reset the pending promise flag
82
+ Log.debug("Resetting pending promise and dequeuing");
83
+ this.#pendingPromise = false;
84
+
85
+ // After the element is reconciled, dequeue the next element
86
+ await this.#dequeue();
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,175 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import { PeprStore } from "./storage";
5
+
6
+ type Unit = "seconds" | "second" | "minute" | "minutes" | "hours" | "hour";
7
+
8
+ export interface Schedule {
9
+ /**
10
+ * * The name of the store
11
+ */
12
+ name: string;
13
+ /**
14
+ * The value associated with a unit of time
15
+ */
16
+ every: number;
17
+ /**
18
+ * The unit of time
19
+ */
20
+ unit: Unit;
21
+ /**
22
+ * The code to run
23
+ */
24
+ run: () => void;
25
+ /**
26
+ * The start time of the schedule
27
+ */
28
+ startTime?: Date | undefined;
29
+
30
+ /**
31
+ * The number of times the schedule has run
32
+ */
33
+ completions?: number | undefined;
34
+ /**
35
+ * Tje intervalID to clear the interval
36
+ */
37
+ intervalID?: NodeJS.Timeout;
38
+ }
39
+
40
+ export class OnSchedule implements Schedule {
41
+ intervalId: NodeJS.Timeout | null = null;
42
+ store: PeprStore | undefined;
43
+ name!: string;
44
+ completions?: number | undefined;
45
+ every: number;
46
+ unit: Unit;
47
+ run!: () => void;
48
+ startTime?: Date | undefined;
49
+ duration: number | undefined;
50
+ lastTimestamp: Date | undefined;
51
+
52
+ constructor(schedule: Schedule) {
53
+ this.name = schedule.name;
54
+ this.run = schedule.run;
55
+ this.every = schedule.every;
56
+ this.unit = schedule.unit;
57
+ this.startTime = schedule?.startTime;
58
+ this.completions = schedule?.completions;
59
+ }
60
+ setStore(store: PeprStore) {
61
+ this.store = store;
62
+ this.startInterval();
63
+ }
64
+ startInterval() {
65
+ this.checkStore();
66
+ this.getDuration();
67
+ this.setupInterval();
68
+ }
69
+ /**
70
+ * Checks the store for this schedule and sets the values if it exists
71
+ * @returns
72
+ */
73
+ checkStore() {
74
+ const result = this.store && this.store.getItem(this.name);
75
+ if (result) {
76
+ const storedSchedule = JSON.parse(result);
77
+ this.completions = storedSchedule?.completions;
78
+ this.startTime = storedSchedule?.startTime;
79
+ this.lastTimestamp = storedSchedule?.lastTimestamp;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Saves the schedule to the store
85
+ * @returns
86
+ */
87
+ saveToStore() {
88
+ const schedule = {
89
+ completions: this.completions,
90
+ startTime: this.startTime,
91
+ lastTimestamp: new Date(),
92
+ name: this.name,
93
+ };
94
+ this.store && this.store.setItem(this.name, JSON.stringify(schedule));
95
+ }
96
+
97
+ /**
98
+ * Gets the durations in milliseconds
99
+ */
100
+ getDuration() {
101
+ switch (this.unit) {
102
+ case "seconds":
103
+ if (this.every < 10) throw new Error("10 Seconds in the smallest interval allowed");
104
+ this.duration = 1000 * this.every;
105
+ break;
106
+ case "minutes":
107
+ case "minute":
108
+ this.duration = 1000 * 60 * this.every;
109
+ break;
110
+ case "hours":
111
+ case "hour":
112
+ this.duration = 1000 * 60 * 60 * this.every;
113
+ break;
114
+ default:
115
+ throw new Error("Invalid time unit");
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Sets up the interval
121
+ */
122
+ setupInterval() {
123
+ const now = new Date();
124
+ let delay: number | undefined;
125
+
126
+ if (this.lastTimestamp && this.startTime) {
127
+ this.startTime = undefined;
128
+ }
129
+
130
+ if (this.startTime) {
131
+ delay = this.startTime.getTime() - now.getTime();
132
+ } else if (this.lastTimestamp && this.duration) {
133
+ const lastTimestamp = new Date(this.lastTimestamp);
134
+ delay = this.duration - (now.getTime() - lastTimestamp.getTime());
135
+ }
136
+
137
+ if (delay === undefined || delay <= 0) {
138
+ this.start();
139
+ } else {
140
+ setTimeout(() => {
141
+ this.start();
142
+ }, delay);
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Starts the interval
148
+ */
149
+ start() {
150
+ this.intervalId = setInterval(() => {
151
+ if (this.completions === 0) {
152
+ this.stop();
153
+ return;
154
+ } else {
155
+ this.run();
156
+
157
+ if (this.completions && this.completions !== 0) {
158
+ this.completions -= 1;
159
+ }
160
+ this.saveToStore();
161
+ }
162
+ }, this.duration);
163
+ }
164
+
165
+ /**
166
+ * Stops the interval
167
+ */
168
+ stop() {
169
+ if (this.intervalId) {
170
+ clearInterval(this.intervalId);
171
+ this.intervalId = null;
172
+ }
173
+ this.store && this.store.removeItem(this.name);
174
+ }
175
+ }