diff --git a/lib/agent/runtime.ts b/lib/agent/runtime.ts index 83c951b..8134a54 100644 --- a/lib/agent/runtime.ts +++ b/lib/agent/runtime.ts @@ -167,7 +167,7 @@ export class Runtime { // If the parent does not exist or the key does not exist // then delete the sensor if (parent == null || !Object.hasOwn(parent, Path.basename(p))) { - this.subscriptions[p].unsubscribe(); + this.subscriptions[p]!.unsubscribe(); delete this.subscriptions[p]; } }); diff --git a/lib/dag.ts b/lib/dag.ts index 5654eff..9822337 100644 --- a/lib/dag.ts +++ b/lib/dag.ts @@ -208,6 +208,7 @@ function traverseCombine( assert(ends.length > 0, 'Malformed DAG found, empty Fork node'); const [res] = ends; + assert(res != null); // Typescript is not smart enough to figure out that res cannot be undefined assert(!res.done, 'Malformed DAG found, disconnected fork branch'); // Combine the results from the branches passing the diff --git a/lib/planner/findPlan.ts b/lib/planner/findPlan.ts index 5ede66a..075b855 100644 --- a/lib/planner/findPlan.ts +++ b/lib/planner/findPlan.ts @@ -133,9 +133,7 @@ function findConflict( ): [PatchOperation, PatchOperation] | undefined { const unique = new Map(); - for (let i = 0; i < ops.length; i++) { - const patches = ops[i]; - + for (const [i, patches] of ops.entries()) { for (const o of patches) { for (const [path, [index, op]] of unique.entries()) { if ( @@ -216,7 +214,7 @@ function tryParallel( // having the fork and empty node, so we need to remove the empty node // and connect the last action in the branch directly to the existing plan if (results.length === 1) { - const branch = results[0]; + const branch = results[0]!; // Find the first node for which the next element is the // empty node created earlier const last = DAG.find( diff --git a/lib/testing/builder.ts b/lib/testing/builder.ts index 8081a19..a7838cb 100644 --- a/lib/testing/builder.ts +++ b/lib/testing/builder.ts @@ -68,7 +68,7 @@ function fromFork(branches: Branch[]): [Node | null, Node | null] { // If there is only a branch, call the branch method if (branches.length === 1) { - return fromBranch(branches[0]); + return fromBranch(branches[0]!); } // For multiple branches, create a fork and diff --git a/lib/testing/mermaid.ts b/lib/testing/mermaid.ts index 80c80c9..ad1dd3c 100644 --- a/lib/testing/mermaid.ts +++ b/lib/testing/mermaid.ts @@ -66,10 +66,11 @@ function fromNode( if (DAG.isFork(node)) { const actions = node.next.filter(PlanAction.is); if (actions.length > 0) { + const [first] = actions; // In this case we just use id of the first action in the // fork as we really should not have multiple fork nodes in the // diagram pointing to the same actions - return DiagramNode.fromId(`j${actions[0].id.substring(0, 7)}`); + return DiagramNode.fromId(`j${first!.id.substring(0, 7)}`); } const forks = node.next.filter(DAG.isFork); @@ -156,7 +157,7 @@ class DiagramAdjacency { // The official parent of a node is the last parent // in the list - return p[p.length - 1]; + return p[p.length - 1]!; } getAll(node: DiagramNode): DiagramNode[] { @@ -260,7 +261,10 @@ class Diagram { ends.forEach(([_, p]) => this.graph.push(` ${p} --> ${join}`)); this.graph.push(` ${join}:::selected`); - const [first] = ends[0]; + const [end] = ends; + assert(end != null); + + const [first] = end; return this.drawPlan(first.next, join); } diff --git a/lib/utils/deep-equal.ts b/lib/utils/deep-equal.ts index abd84bc..ed60726 100644 --- a/lib/utils/deep-equal.ts +++ b/lib/utils/deep-equal.ts @@ -1,3 +1,5 @@ +import { assert } from '../assert'; + function isObject(value: unknown): value is object { return typeof value === 'object' && value !== null; } @@ -33,6 +35,11 @@ export function deepEqual(value: T, other: T): boolean { const [vProps, oProps] = [value, other].map( (a) => Object.getOwnPropertyNames(a) as Array, ); + + // This will never fail but it prevents the compiler from + // complaining + assert(vProps != null && oProps != null); + if (vProps.length !== oProps.length) { // If the property lists are different lengths we don't need // to check any further diff --git a/package.json b/package.json index 66d1c53..5017458 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "tar-stream": "3.0.0", "ts-node": "^10.9.1", "tsconfig-paths": "^4.1.1", - "typescript": "^5.5.2" + "typescript": "^5.5.4" }, "dependencies": { "mahler-wasm": "^0.1.0" diff --git a/tests/composer/tasks.ts b/tests/composer/tasks.ts index 68b26fb..a81bee9 100644 --- a/tests/composer/tasks.ts +++ b/tests/composer/tasks.ts @@ -116,7 +116,7 @@ export const installService = App.task({ })(); const s: Service = { image: - (await docker.getImage(existing.Image).inspect()).RepoTags[0] || + (await docker.getImage(existing.Image).inspect()).RepoTags[0] ?? existing.Image, startedAt: new Date(existing.State.StartedAt), createdAt: new Date(existing.Created), diff --git a/tests/orchestrator/tasks.spec.ts b/tests/orchestrator/tasks.spec.ts index c77836d..ae6102d 100644 --- a/tests/orchestrator/tasks.spec.ts +++ b/tests/orchestrator/tasks.spec.ts @@ -58,7 +58,7 @@ describe('orchestrator/tasks', () => { s = await doFetch(s); expect(s.apps['a0']).to.not.be.undefined; expect(s.images).to.have.property('a0_my-service:r0'); - const { dockerId: imageId } = s.images['a0_my-service:r0']; + const { dockerId: imageId } = s.images['a0_my-service:r0']!; expect(imageId).to.not.be.undefined; expect(await docker.getImage(imageId!).inspect()) .to.have.property('RepoTags') @@ -138,12 +138,12 @@ describe('orchestrator/tasks', () => { it('should create a container', async () => { let s = await doInstall(s0); - expect(s.apps['a0'].releases['r0']).to.not.be.undefined; - expect(s.apps['a0'].releases['r0'].services['my-service']).to.not.be + expect(s.apps['a0']!.releases['r0']).to.not.be.undefined; + expect(s.apps['a0']!.releases['r0']!.services['my-service']).to.not.be .undefined; const { containerId } = - s.apps['a0'].releases['r0'].services['my-service']; + s.apps['a0']!.releases['r0']!.services['my-service']!; expect(containerId).to.not.be.undefined; const container = await docker.getContainer(containerId!).inspect(); @@ -153,8 +153,8 @@ describe('orchestrator/tasks', () => { // Installing again with an outated state should // not create a new container s = await doInstall(s0); - expect(s.apps['a0'].releases['r0']).to.not.be.undefined; - expect(s.apps['a0'].releases['r0'].services['my-service']).to.not.be + expect(s.apps['a0']!.releases['r0']).to.not.be.undefined; + expect(s.apps['a0']!.releases['r0']!.services['my-service']).to.not.be .undefined; expect(await docker.getContainer(containerId!).inspect()) @@ -223,15 +223,15 @@ describe('orchestrator/tasks', () => { it('should start a container', async () => { let s = await doStart(s0); - expect(s.apps['a0'].releases['r0']).to.not.be.undefined; - expect(s.apps['a0'].releases['r0'].services['my-service']).to.not.be + expect(s.apps['a0']!.releases['r0']).to.not.be.undefined; + expect(s.apps['a0']!.releases['r0']!.services['my-service']).to.not.be .undefined; expect( - s.apps['a0'].releases['r0'].services['my-service'].status, + s.apps['a0']!.releases['r0']!.services['my-service']!.status, ).to.equal('running'); const { containerId } = - s.apps['a0'].releases['r0'].services['my-service']; + s.apps['a0']!.releases['r0']!.services['my-service']!; expect(containerId).to.not.be.undefined; const container = await docker.getContainer(containerId!).inspect(); @@ -242,8 +242,8 @@ describe('orchestrator/tasks', () => { // Starting again with an outated state should // not start the container again s = await doStart(s0); - expect(s.apps['a0'].releases['r0']).to.not.be.undefined; - expect(s.apps['a0'].releases['r0'].services['my-service']).to.not.be + expect(s.apps['a0']!.releases['r0']).to.not.be.undefined; + expect(s.apps['a0']!.releases['r0']!.services['my-service']).to.not.be .undefined; expect(await docker.getContainer(containerId!).inspect()) diff --git a/tests/orchestrator/tasks.ts b/tests/orchestrator/tasks.ts index 04b4564..e309c08 100644 --- a/tests/orchestrator/tasks.ts +++ b/tests/orchestrator/tasks.ts @@ -273,7 +273,7 @@ export const startService = Task.of().from({ service, { releaseUuid, appUuid, serviceName, system: device, target }, ) => { - const { releases } = device.apps[appUuid]; + const { releases } = device.apps[appUuid]!; // The task can be applied if the following conditions are met: return ( @@ -287,7 +287,7 @@ export const startService = Task.of().from({ // service cannot be running Object.keys(releases) .filter((u) => u !== releaseUuid) - .every((u) => releases[u].services[serviceName]?.status !== 'running') + .every((u) => releases[u]!.services[serviceName]?.status !== 'running') ); }, effect: (service) => { @@ -341,10 +341,13 @@ const renameServiceContainer = Task.of().from({ _, { appUuid, releaseUuid, serviceName, system: device, target }, ) => { - const { releases } = device.apps[appUuid]; + const { releases } = device.apps[appUuid]!; const [currentRelease] = Object.keys(releases).filter( (u) => u !== releaseUuid, ); + if (currentRelease == null) { + return false; + } const currService = releases[currentRelease]?.services[serviceName]; return ( currService != null && @@ -353,7 +356,7 @@ const renameServiceContainer = Task.of().from({ ); }, effect: (service, { system: device, appUuid, serviceName, releaseUuid }) => { - const { releases } = device.apps[appUuid]; + const { releases } = device.apps[appUuid]!; const currRelease = Object.keys(releases).find((u) => u !== releaseUuid)!; const currService = releases[currRelease]?.services[serviceName]; @@ -364,10 +367,9 @@ const renameServiceContainer = Task.of().from({ service, { system: device, appUuid, serviceName, releaseUuid }, ) => { - const { releases } = device.apps[appUuid]; + const { releases } = device.apps[appUuid]!; const currRelease = Object.keys(releases).find((u) => u !== releaseUuid)!; - - const currService = releases[currRelease]?.services[serviceName]; + const currService = releases[currRelease]!.services[serviceName]!; // Rename the container await docker.getContainer(currService.containerId!).rename({ @@ -399,10 +401,14 @@ export const migrateService = Task.of().from({ _, { appUuid, releaseUuid, serviceName, system: device, target }, ) => { - const { releases } = device.apps[appUuid]; + const { releases } = device.apps[appUuid]!; const [currentRelease] = Object.keys(releases).filter( (u) => u !== releaseUuid, ); + if (!currentRelease) { + return false; + } + const currService = releases[currentRelease]?.services[serviceName]; return ( currService != null && @@ -414,7 +420,7 @@ export const migrateService = Task.of().from({ _, { system: device, appUuid, serviceName, releaseUuid, target }, ) => { - const { releases } = device.apps[appUuid]; + const { releases } = device.apps[appUuid]!; const currRelease = Object.keys(releases).find((u) => u !== releaseUuid)!; return [ renameServiceContainer({ target, appUuid, serviceName, releaseUuid }), @@ -443,7 +449,7 @@ export const stopService = Task.of().from({ service, { system: device, appUuid, releaseUuid, serviceName }, ) => { - const { releases } = device.apps[appUuid]; + const { releases } = device.apps[appUuid]!; return ( service?.containerId != null && service?.status === 'running' && @@ -451,7 +457,7 @@ export const stopService = Task.of().from({ .filter((u) => u !== releaseUuid) .every( // If there are equivalent services from other releases they should at least have a container - (u) => releases[u].services[serviceName]?.containerId != null, + (u) => releases[u]!.services[serviceName]?.containerId != null, ) ); }, diff --git a/tsconfig.json b/tsconfig.json index 94bbb39..160af53 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,7 @@ "target": "es2022", "declaration": true, "skipLibCheck": true, + "noUncheckedIndexedAccess": true, "paths": { "mahler": [ "lib/index.ts"