@norskvideo/norsk-auto-manager 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.
@@ -0,0 +1,232 @@
1
+ "use strict";
2
+ // Phase D1 — runtime validation of AutoSettings. Hand-rolled in the
3
+ // same shape as `validateBundle` (validation.ts) rather than dragging
4
+ // in zod for a small surface. Returns a list of structured errors —
5
+ // empty means valid.
6
+ //
7
+ // `AutoManager.run` invokes this at entry and throws
8
+ // `AutoSettingsValidationError` if anything fails. Catches typos and
9
+ // shape mistakes at boot rather than mid-flight.
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.AutoSettingsValidationError = void 0;
12
+ exports.validateAutoSettings = validateAutoSettings;
13
+ /**
14
+ * Validate an `AutoSettings` value at boot. Returns all errors found
15
+ * (does not short-circuit). Callbacks are not validated — TypeScript
16
+ * structural typing already covers them as well as a runtime check
17
+ * usefully can. The point of this validator is to catch
18
+ * configuration mistakes operators make (typos in pool names, bad
19
+ * enum values, negative timeouts), not to second-guess the type
20
+ * system.
21
+ *
22
+ * @public
23
+ */
24
+ function validateAutoSettings(settings) {
25
+ const errors = [];
26
+ const ctx = new Ctx(errors);
27
+ ctx.checkPositive(settings.pendingWindow, "pendingWindow");
28
+ ctx.checkPositive(settings.removeStoppedNodesAfter, "removeStoppedNodesAfter");
29
+ // Flat optional tunables.
30
+ ctx.checkOptionalPositive(settings.rejectionBackoffMs, "rejectionBackoffMs");
31
+ ctx.checkOptionalPositive(settings.rejectionEscalationCount, "rejectionEscalationCount");
32
+ ctx.checkOptionalPositive(settings.failureBackoffMs, "failureBackoffMs");
33
+ ctx.checkOptionalPositive(settings.maxRestartsPerHour, "maxRestartsPerHour");
34
+ ctx.checkOptionalPositive(settings.jobStartupGraceMs, "jobStartupGraceMs");
35
+ ctx.checkOptionalPositive(settings.hotSpareReconcileIntervalMs, "hotSpareReconcileIntervalMs");
36
+ // Placement pools.
37
+ const knownPoolNames = new Set();
38
+ if (!Array.isArray(settings.placementPools)) {
39
+ ctx.add("invalidType", "placementPools", "must be an array");
40
+ }
41
+ else {
42
+ settings.placementPools.forEach((pool, i) => {
43
+ validatePool(pool, `placementPools[${i}]`, knownPoolNames, ctx);
44
+ });
45
+ }
46
+ // Typed configs.
47
+ if (settings.bands !== undefined) {
48
+ validateBandConfig(settings.bands, "bands", ctx);
49
+ }
50
+ if (settings.placement !== undefined) {
51
+ validatePlacementConfig(settings.placement, "placement", ctx);
52
+ }
53
+ if (settings.hotSpares !== undefined) {
54
+ if (!Array.isArray(settings.hotSpares)) {
55
+ ctx.add("invalidType", "hotSpares", "must be an array");
56
+ }
57
+ else {
58
+ settings.hotSpares.forEach((entry, i) => {
59
+ validateHotSpare(entry, `hotSpares[${i}]`, knownPoolNames, settings.placementPools ?? [], ctx);
60
+ });
61
+ }
62
+ }
63
+ return errors;
64
+ }
65
+ function validatePool(pool, path, knownPoolNames, ctx) {
66
+ if (typeof pool.name !== "string" || pool.name.length === 0) {
67
+ ctx.add("invalidType", `${path}.name`, "must be a non-empty string");
68
+ }
69
+ else if (knownPoolNames.has(pool.name)) {
70
+ ctx.add("duplicatePool", `${path}.name`, `duplicate pool name "${pool.name}"`);
71
+ }
72
+ else {
73
+ knownPoolNames.add(pool.name);
74
+ }
75
+ if (!Array.isArray(pool.tiers) || pool.tiers.length === 0) {
76
+ ctx.add("incompatibleConfig", `${path}.tiers`, `pool needs at least one tier`);
77
+ return;
78
+ }
79
+ const tierNames = new Set();
80
+ let fixedTierCount = 0;
81
+ pool.tiers.forEach((tier, i) => {
82
+ const tp = `${path}.tiers[${i}]`;
83
+ if (typeof tier.name !== "string" || tier.name.length === 0) {
84
+ ctx.add("invalidType", `${tp}.name`, "must be a non-empty string");
85
+ }
86
+ else if (tierNames.has(tier.name)) {
87
+ ctx.add("duplicatePool", `${tp}.name`, `duplicate tier name "${tier.name}"`);
88
+ }
89
+ else {
90
+ tierNames.add(tier.name);
91
+ }
92
+ if (tier.scaleOut === "fixed")
93
+ fixedTierCount++;
94
+ validateTier(tier, tp, ctx);
95
+ });
96
+ // nodeMatchesTier maps an untagged (pre-registered cluster) node to the
97
+ // pool's fixed tier, so that mapping must be unambiguous.
98
+ if (fixedTierCount > 1) {
99
+ ctx.add("incompatibleConfig", `${path}.tiers`, `pool may have at most one "fixed" tier (found ${fixedTierCount})`);
100
+ }
101
+ }
102
+ function validateTier(tier, path, ctx) {
103
+ if (!["aws", "oci", "local-servers"].includes(tier.kind)) {
104
+ ctx.add("invalidEnumValue", `${path}.kind`, `must be one of "aws" | "oci" | "local-servers"`);
105
+ }
106
+ if (!["binpack", "spread"].includes(tier.packingStrategy)) {
107
+ ctx.add("invalidEnumValue", `${path}.packingStrategy`, `must be "binpack" or "spread"`);
108
+ }
109
+ if (!["elastic", "fixed"].includes(tier.scaleOut)) {
110
+ ctx.add("invalidEnumValue", `${path}.scaleOut`, `must be "elastic" or "fixed"`);
111
+ }
112
+ if (tier.kind === "local-servers" && tier.scaleOut === "elastic") {
113
+ ctx.add("incompatibleConfig", `${path}`, `cluster tiers cannot be "elastic" (no auto-scale)`);
114
+ }
115
+ if (!Array.isArray(tier.candidateInstanceTypes)) {
116
+ ctx.add("invalidType", `${path}.candidateInstanceTypes`, "must be an array");
117
+ }
118
+ else {
119
+ if (tier.scaleOut === "elastic" && tier.candidateInstanceTypes.length === 0) {
120
+ ctx.add("incompatibleConfig", `${path}.candidateInstanceTypes`, `elastic tier needs at least one candidate instance type`);
121
+ }
122
+ tier.candidateInstanceTypes.forEach((c, i) => {
123
+ const p = `${path}.candidateInstanceTypes[${i}]`;
124
+ if (typeof c.instanceType !== "string" || c.instanceType.length === 0) {
125
+ ctx.add("invalidType", `${p}.instanceType`, "must be non-empty string");
126
+ }
127
+ if (typeof c.totalCapacity !== "number" || c.totalCapacity <= 0) {
128
+ ctx.add("outOfRange", `${p}.totalCapacity`, "must be > 0");
129
+ }
130
+ if (typeof c.totalCores !== "number" ||
131
+ c.totalCores <= 0 ||
132
+ !Number.isInteger(c.totalCores)) {
133
+ ctx.add("outOfRange", `${p}.totalCores`, "must be a positive integer");
134
+ }
135
+ });
136
+ }
137
+ }
138
+ function validateBandConfig(bands, path, ctx) {
139
+ if (bands.gold) {
140
+ if (bands.gold.onFailure !== "restart") {
141
+ ctx.add("invalidEnumValue", `${path}.gold.onFailure`, `must be "restart"`);
142
+ }
143
+ if (!["preferHotSpare", "any"].includes(bands.gold.restartPlacement)) {
144
+ ctx.add("invalidEnumValue", `${path}.gold.restartPlacement`, `must be "preferHotSpare" or "any"`);
145
+ }
146
+ ctx.checkOptionalPositive(bands.gold.maxRestartsPerHour, `${path}.gold.maxRestartsPerHour`);
147
+ }
148
+ if (bands.silver) {
149
+ if (bands.silver.onFailure !== "restart") {
150
+ ctx.add("invalidEnumValue", `${path}.silver.onFailure`, `must be "restart"`);
151
+ }
152
+ if (bands.silver.restartPlacement !== "any") {
153
+ ctx.add("invalidEnumValue", `${path}.silver.restartPlacement`, `must be "any"`);
154
+ }
155
+ ctx.checkOptionalPositive(bands.silver.maxRestartsPerHour, `${path}.silver.maxRestartsPerHour`);
156
+ }
157
+ if (bands.bronze) {
158
+ if (bands.bronze.onFailure !== "drop") {
159
+ ctx.add("invalidEnumValue", `${path}.bronze.onFailure`, `must be "drop"`);
160
+ }
161
+ }
162
+ }
163
+ function validatePlacementConfig(cfg, path, ctx) {
164
+ ctx.checkOptionalPositive(cfg.rejectionBackoffMs, `${path}.rejectionBackoffMs`);
165
+ ctx.checkOptionalPositive(cfg.rejectionEscalationCount, `${path}.rejectionEscalationCount`);
166
+ ctx.checkOptionalPositive(cfg.failureBackoffMs, `${path}.failureBackoffMs`);
167
+ ctx.checkOptionalPositive(cfg.jobStartupGraceMs, `${path}.jobStartupGraceMs`);
168
+ }
169
+ function validateHotSpare(cfg, path, knownPoolNames, placementPools, ctx) {
170
+ if (typeof cfg.pool !== "string" || cfg.pool.length === 0) {
171
+ ctx.add("invalidType", `${path}.pool`, "must be a non-empty string");
172
+ }
173
+ else if (!knownPoolNames.has(cfg.pool)) {
174
+ ctx.add("unknownPool", `${path}.pool`, `pool "${cfg.pool}" is not declared in placementPools`);
175
+ }
176
+ if (typeof cfg.targetCount !== "number" ||
177
+ cfg.targetCount < 0 ||
178
+ !Number.isInteger(cfg.targetCount)) {
179
+ ctx.add("outOfRange", `${path}.targetCount`, `must be a non-negative integer (got ${cfg.targetCount})`);
180
+ }
181
+ if (cfg.forBand !== undefined && !["gold", "silver", "bronze"].includes(cfg.forBand)) {
182
+ ctx.add("invalidEnumValue", `${path}.forBand`, `must be "gold" | "silver" | "bronze"`);
183
+ }
184
+ if (cfg.spec === undefined || typeof cfg.spec !== "object") {
185
+ ctx.add("missingRequired", `${path}.spec`, "spec is required");
186
+ return;
187
+ }
188
+ // A pool with an elastic tier needs an instance type to provision against.
189
+ const pool = placementPools.find((p) => p.name === cfg.pool);
190
+ if (pool &&
191
+ pool.tiers.some((t) => t.scaleOut === "elastic") &&
192
+ !cfg.spec.instanceType) {
193
+ ctx.add("missingRequired", `${path}.spec.instanceType`, `elastic pool spares require spec.instanceType`);
194
+ }
195
+ }
196
+ class Ctx {
197
+ constructor(errors) {
198
+ this.errors = errors;
199
+ }
200
+ add(code, path, message) {
201
+ this.errors.push({ code, path, message });
202
+ }
203
+ checkPositive(value, path) {
204
+ if (typeof value !== "number" || value <= 0) {
205
+ this.add("outOfRange", path, `must be a positive number (got ${value})`);
206
+ }
207
+ }
208
+ checkOptionalPositive(value, path) {
209
+ if (value === undefined)
210
+ return;
211
+ if (typeof value !== "number" || value <= 0) {
212
+ this.add("outOfRange", path, `must be a positive number when set (got ${value})`);
213
+ }
214
+ }
215
+ }
216
+ /**
217
+ * Thrown by `AutoManager.run` when `validateAutoSettings` finds
218
+ * configuration errors. The `.errors` property carries the structured
219
+ * details; `.message` contains a human-readable summary.
220
+ *
221
+ * @public
222
+ */
223
+ class AutoSettingsValidationError extends Error {
224
+ constructor(errors) {
225
+ super(`AutoSettings validation failed (${errors.length} error${errors.length === 1 ? "" : "s"}):\n ` +
226
+ errors.map((e) => `${e.path}: ${e.message}`).join("\n "));
227
+ this.errors = errors;
228
+ this.name = "AutoSettingsValidationError";
229
+ }
230
+ }
231
+ exports.AutoSettingsValidationError = AutoSettingsValidationError;
232
+ //# sourceMappingURL=settingsValidation.js.map
@@ -0,0 +1,17 @@
1
+ /** @public */
2
+ export declare function norskHost(): string;
3
+ /** @public */
4
+ export declare function norskPort(): string;
5
+ /** @public */
6
+ export declare function publicUrlPrefix(): string;
7
+ /** @public */
8
+ export declare function debugUrlPrefix(): string;
9
+ /** @public */
10
+ export declare function clientHostExternal(): string;
11
+ /** @public */
12
+ export declare function clientPortExternal(): string;
13
+ /** @public */
14
+ export declare function clientHostInternal(): string;
15
+ /** @public */
16
+ export declare function clientPortInternal(): string;
17
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1,220 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.unwrapEither = exports.isRight = exports.isLeft = exports.makeRight = exports.makeLeft = void 0;
37
+ exports.provideFull = provideFull;
38
+ exports.exhaustiveCheck = exhaustiveCheck;
39
+ exports.mkCase = mkCase;
40
+ exports.mkMessageCase = mkMessageCase;
41
+ exports.mapOptional = mapOptional;
42
+ exports.debuglog = debuglog;
43
+ exports.errorlog = errorlog;
44
+ exports.invokeOnCreate = invokeOnCreate;
45
+ exports.mandatory = mandatory;
46
+ exports.norskHost = norskHost;
47
+ exports.norskPort = norskPort;
48
+ exports.publicUrlPrefix = publicUrlPrefix;
49
+ exports.debugUrlPrefix = debugUrlPrefix;
50
+ exports.clientHostExternal = clientHostExternal;
51
+ exports.clientPortExternal = clientPortExternal;
52
+ exports.clientHostInternal = clientHostInternal;
53
+ exports.clientPortInternal = clientPortInternal;
54
+ const util = __importStar(require("util"));
55
+ const protobuf_1 = require("@bufbuild/protobuf");
56
+ /**
57
+ * @internal
58
+ *
59
+ * Ask TypeScript to tell us when we've left the fields out of a message.
60
+ *
61
+ * Usage:
62
+ * provideFull(MyProtobufMessageSchema, { myField })
63
+ * instead of
64
+ * create(MyProtobufMessageSchema, { myField })
65
+ */
66
+ function provideFull(schema, v) {
67
+ // This no longer does it's job I'm just trying to compile as a first step
68
+ return (0, protobuf_1.create)(schema, v);
69
+ }
70
+ /** @internal
71
+ *
72
+ * unreachable
73
+ */
74
+ function exhaustiveCheck(a) {
75
+ throw new Error(`Unhandled case: ${a}`);
76
+ }
77
+ /** @internal */
78
+ function mkCase(obj) {
79
+ const keys = Object.keys(obj);
80
+ if (keys.length !== 1)
81
+ throw new Error("Bad case");
82
+ return { case: keys[0], value: obj[keys[0]] };
83
+ }
84
+ /** @internal */
85
+ function mkMessageCase(obj) {
86
+ return { message: mkCase(obj) };
87
+ }
88
+ /** @internal */
89
+ function mapOptional(fn, value) {
90
+ return value !== undefined ? fn(value) : undefined;
91
+ }
92
+ /** @internal */
93
+ function debuglog(msg, ...param) {
94
+ util.debuglog("norsk")(util.formatWithOptions({ maxArrayLength: null, depth: null, colors: true }, "[" + new Date().toISOString() + "] " + msg, ...param));
95
+ }
96
+ /** @internal */
97
+ function errorlog(msg, ...param) {
98
+ util.debuglog("norsk")(util.formatWithOptions({ maxArrayLength: null, depth: null, colors: true }, "[" + new Date().toISOString() + "] " + msg, ...param));
99
+ }
100
+ /**
101
+ * @internal
102
+ * Invoke a node's `onCreate` callback, warning loudly if it returned a Promise.
103
+ *
104
+ * `onCreate` MUST be synchronous (its declared type is `(node) => void`). Frames
105
+ * flow as soon as it returns, so an `async` onCreate doesn't block them — it
106
+ * suspends at its first `await` and the SDK proceeds, meaning any subscriptions
107
+ * set up after that `await` race the node's first frames. This catches the
108
+ * `async`-onCreate footgun at runtime for both TypeScript and plain-JS clients
109
+ * (no lint or type-checking required), at the cost of one `typeof` per node.
110
+ */
111
+ function invokeOnCreate(onCreate, node) {
112
+ if (!onCreate)
113
+ return;
114
+ const result = onCreate(node);
115
+ if (result != null && typeof result.then === "function") {
116
+ // console.warn (not the NODE_DEBUG-gated errorlog) so this is always visible —
117
+ // the whole point is to surface the footgun without any opt-in.
118
+ console.warn("[norsk-sdk] onCreate for node '%s' returned a Promise — onCreate must be SYNCHRONOUS (was it declared `async`?). " +
119
+ "Frames flow as soon as onCreate returns, so an async onCreate does NOT block them, and any subscriptions " +
120
+ "set up after an `await` will race the node's first frames. Do subscription setup synchronously in onCreate.", node?.id ?? "<unknown>");
121
+ }
122
+ }
123
+ /** @internal */
124
+ function mandatory(value, name) {
125
+ if (value !== undefined) {
126
+ return value;
127
+ }
128
+ else {
129
+ if (name === undefined) {
130
+ throw new Error();
131
+ }
132
+ else {
133
+ throw new Error(`Mandatory variable '${name}' is undefined`);
134
+ }
135
+ }
136
+ }
137
+ /** @internal */
138
+ const makeLeft = (value) => ({ left: value });
139
+ exports.makeLeft = makeLeft;
140
+ /** @internal */
141
+ const makeRight = (value) => ({ right: value });
142
+ exports.makeRight = makeRight;
143
+ /** @internal */
144
+ const isLeft = (e) => {
145
+ return e.left !== undefined;
146
+ };
147
+ exports.isLeft = isLeft;
148
+ /** @internal */
149
+ const isRight = (e) => {
150
+ return e.right !== undefined;
151
+ };
152
+ exports.isRight = isRight;
153
+ /** @internal */
154
+ const unwrapEither = ({ left, right, }) => {
155
+ if (right !== undefined && left !== undefined) {
156
+ throw new Error(`Received both left and right values at runtime when opening an Either\nLeft: ${JSON.stringify(left)}\nRight: ${JSON.stringify(right)}`);
157
+ /*
158
+ We're throwing in this function because this can only occur at runtime if something
159
+ happens that the TypeScript compiler couldn't anticipate. That means the application
160
+ is in an unexpected state and we should terminate immediately.
161
+ */
162
+ }
163
+ if (left !== undefined) {
164
+ return left; // Typescript is getting confused and returning this type as `T | undefined` unless we add the type assertion
165
+ }
166
+ if (right !== undefined) {
167
+ return right;
168
+ }
169
+ throw new Error(`Received no left or right values at runtime when opening Either`);
170
+ };
171
+ exports.unwrapEither = unwrapEither;
172
+ /** @public */
173
+ function norskHost() {
174
+ if (process.env.NORSK_HOST)
175
+ return process.env.NORSK_HOST;
176
+ return "127.0.0.1";
177
+ }
178
+ /** @public */
179
+ function norskPort() {
180
+ if (process.env.NORSK_PORT)
181
+ return process.env.NORSK_PORT;
182
+ return "6790";
183
+ }
184
+ /** @public */
185
+ function publicUrlPrefix() {
186
+ if (process.env.PUBLIC_URL_PREFIX)
187
+ return process.env.PUBLIC_URL_PREFIX;
188
+ return "http://127.0.0.1:8080";
189
+ }
190
+ /** @public */
191
+ function debugUrlPrefix() {
192
+ if (process.env.DEBUG_URL_PREFIX)
193
+ return process.env.DEBUG_URL_PREFIX;
194
+ return "http://" + norskHost() + ":6791";
195
+ }
196
+ /** @public */
197
+ function clientHostExternal() {
198
+ if (process.env.CLIENT_HOST_EXTERNAL)
199
+ return process.env.CLIENT_HOST_EXTERNAL;
200
+ return "127.0.0.1";
201
+ }
202
+ /** @public */
203
+ function clientPortExternal() {
204
+ if (process.env.CLIENT_PORT_EXTERNAL)
205
+ return process.env.CLIENT_PORT_EXTERNAL;
206
+ return "3000";
207
+ }
208
+ /** @public */
209
+ function clientHostInternal() {
210
+ if (process.env.CLIENT_HOST_INTERNAL)
211
+ return process.env.CLIENT_HOST_INTERNAL;
212
+ return "127.0.0.1";
213
+ }
214
+ /** @public */
215
+ function clientPortInternal() {
216
+ if (process.env.CLIENT_PORT_INTERNAL)
217
+ return process.env.CLIENT_PORT_INTERNAL;
218
+ return "3000";
219
+ }
220
+ //# sourceMappingURL=utils.js.map