@aeolus-fleet/core 0.25.3 → 0.26.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/dist/adapters/prisma/health.d.ts +3 -0
- package/dist/adapters/prisma/health.js +122 -0
- package/dist/adapters/prisma/health.js.map +1 -0
- package/dist/adapters/prisma/history.js +7 -5
- package/dist/adapters/prisma/history.js.map +1 -1
- package/dist/adapters/prisma/installation.js +2 -1
- package/dist/adapters/prisma/installation.js.map +1 -1
- package/dist/adapters/trpc/context.d.ts +2 -0
- package/dist/adapters/trpc/fleet.d.ts +50 -0
- package/dist/adapters/trpc/fleet.js +6 -1
- package/dist/adapters/trpc/fleet.js.map +1 -1
- package/dist/adapters/trpc/health.d.ts +39 -0
- package/dist/adapters/trpc/health.js +14 -0
- package/dist/adapters/trpc/health.js.map +1 -0
- package/dist/adapters/trpc/installation.d.ts +76 -0
- package/dist/adapters/trpc/installation.js +8 -2
- package/dist/adapters/trpc/installation.js.map +1 -1
- package/dist/adapters/trpc/router.d.ts +126 -0
- package/dist/adapters/trpc/ship-protocol.js +10 -9
- package/dist/adapters/trpc/ship-protocol.js.map +1 -1
- package/dist/adapters/trpc/ship.d.ts +1 -0
- package/dist/domain/messaging/delivery.d.ts +3 -1
- package/dist/domain/messaging/delivery.js +4 -3
- package/dist/domain/messaging/delivery.js.map +1 -1
- package/dist/domain/messaging/overdue.d.ts +7 -0
- package/dist/domain/messaging/overdue.js +9 -4
- package/dist/domain/messaging/overdue.js.map +1 -1
- package/dist/domain/messaging/public.d.ts +1 -1
- package/dist/domain/messaging/public.js +3 -1
- package/dist/domain/messaging/public.js.map +1 -1
- package/dist/domain/messaging/report-not-handled.d.ts +3 -1
- package/dist/domain/messaging/report-not-handled.js +2 -1
- package/dist/domain/messaging/report-not-handled.js.map +1 -1
- package/dist/domain/messaging/report-turn-end.d.ts +1 -0
- package/dist/domain/messaging/report-turn-end.js +2 -2
- package/dist/domain/messaging/report-turn-end.js.map +1 -1
- package/dist/domain/registry/get-installation-fleet.d.ts +2 -0
- package/dist/domain/registry/get-installation-fleet.js +5 -1
- package/dist/domain/registry/get-installation-fleet.js.map +1 -1
- package/dist/domain/registry/installation-fleet.d.ts +4 -1
- package/dist/domain/registry/installation-fleet.js +2 -2
- package/dist/domain/registry/installation-fleet.js.map +1 -1
- package/dist/domain/registry/list-installation-fleets.d.ts +4 -2
- package/dist/domain/registry/list-installation-fleets.js +5 -3
- package/dist/domain/registry/list-installation-fleets.js.map +1 -1
- package/dist/domain/shared/health.d.ts +84 -0
- package/dist/domain/shared/health.js +59 -0
- package/dist/domain/shared/health.js.map +1 -0
- package/dist/domain/shared/history.d.ts +4 -0
- package/dist/domain/shared/history.js.map +1 -1
- package/dist/domain/shared/read-health.d.ts +14 -0
- package/dist/domain/shared/read-health.js +11 -0
- package/dist/domain/shared/read-health.js.map +1 -0
- package/dist/domain/shared/stuck.d.ts +104 -0
- package/dist/domain/shared/stuck.js +187 -0
- package/dist/domain/shared/stuck.js.map +1 -0
- package/dist/wiring.d.ts +2 -0
- package/dist/wiring.js +15 -2
- package/dist/wiring.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"report-turn-end.js","sourceRoot":"","sources":["../../../src/domain/messaging/report-turn-end.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAqC,MAAM,uBAAuB,CAAC;AAGrF,OAAO,EAAE,MAAM,EAAoB,MAAM,qBAAqB,CAAC;AAC/D,OAAO,EAAE,WAAW,EAAiB,MAAM,qBAAqB,CAAC;AACjE,OAAO,EAAE,EAAE,EAAe,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,OAAO,EAAqC,MAAM,eAAe,CAAC;AAU3E;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,IAA6D,EAC7D,
|
|
1
|
+
{"version":3,"file":"report-turn-end.js","sourceRoot":"","sources":["../../../src/domain/messaging/report-turn-end.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAqC,MAAM,uBAAuB,CAAC;AAGrF,OAAO,EAAE,MAAM,EAAoB,MAAM,qBAAqB,CAAC;AAC/D,OAAO,EAAE,WAAW,EAAiB,MAAM,qBAAqB,CAAC;AACjE,OAAO,EAAE,EAAE,EAAe,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,OAAO,EAAqC,MAAM,eAAe,CAAC;AAU3E;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,IAA6D,EAC7D,MAA6E;IAE7E,MAAM,EAAE,EAAE,EAAE,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,MAAM,EAAE,GAAG,MAAM,CAAC;IAC/C,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;IACvC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACf,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IAC7E,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,MAAM,CAAC,oBAAoB,EAAE,YAAY,UAAU,iBAAiB,CAAC,CAAC;IAC/E,CAAC;IACD,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,GAAG,MAAM,EAAE,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IAC3E,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QAChB,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC;IAClD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,MAAM,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACtC,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,WAAW,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,KAAK,CAAC,CAAC;IACjE,CAAC;IACD,OAAO,EAAE,CAAC,SAAS,CAAC,CAAC;AACvB,CAAC"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { FleetId } from '@aeolus-fleet/common';
|
|
2
2
|
import type { Clock } from '../shared/clock.js';
|
|
3
3
|
import { type DomainError } from '../shared/errors.js';
|
|
4
|
+
import { type HealthRecords } from '../shared/health.js';
|
|
4
5
|
import { type Result } from '../shared/result.js';
|
|
5
6
|
import { type InstallationFleet } from './installation-fleet.js';
|
|
6
7
|
import type { InstallationFleets, InstallationSettingsRepository } from './ports.js';
|
|
@@ -11,5 +12,6 @@ export type GetInstallationFleet = (input: {
|
|
|
11
12
|
export declare function createGetInstallationFleet(deps: {
|
|
12
13
|
fleets: InstallationFleets;
|
|
13
14
|
settings: InstallationSettingsRepository;
|
|
15
|
+
health: HealthRecords;
|
|
14
16
|
clock: Clock;
|
|
15
17
|
}): GetInstallationFleet;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { refuse } from '../shared/errors.js';
|
|
2
|
+
import { readHealthOf } from '../shared/health.js';
|
|
2
3
|
import { ok } from '../shared/result.js';
|
|
3
4
|
import { installationFleet, messageWindow } from './installation-fleet.js';
|
|
4
5
|
/** Use case: one fleet of the installation, described as the list describes it. */
|
|
@@ -6,7 +7,10 @@ export function createGetInstallationFleet(deps) {
|
|
|
6
7
|
return async ({ fleetId }) => {
|
|
7
8
|
const now = deps.clock.now();
|
|
8
9
|
const facts = await deps.fleets.find(fleetId, messageWindow(now));
|
|
9
|
-
|
|
10
|
+
if (!facts) {
|
|
11
|
+
return refuse('FLEET_NOT_FOUND', `The installation hosts no fleet ${fleetId}`);
|
|
12
|
+
}
|
|
13
|
+
return ok(installationFleet(facts, { now, installation: await deps.settings.read(), health: await readHealthOf(deps.health, { fleetId, now }) }));
|
|
10
14
|
};
|
|
11
15
|
}
|
|
12
16
|
//# sourceMappingURL=get-installation-fleet.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-installation-fleet.js","sourceRoot":"","sources":["../../../src/domain/registry/get-installation-fleet.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAoB,MAAM,qBAAqB,CAAC;AAC/D,OAAO,EAAE,EAAE,EAAe,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAA0B,MAAM,yBAAyB,CAAC;AAKnG,mFAAmF;AACnF,MAAM,UAAU,0BAA0B,CAAC,
|
|
1
|
+
{"version":3,"file":"get-installation-fleet.js","sourceRoot":"","sources":["../../../src/domain/registry/get-installation-fleet.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAoB,MAAM,qBAAqB,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAsB,MAAM,qBAAqB,CAAC;AACvE,OAAO,EAAE,EAAE,EAAe,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAA0B,MAAM,yBAAyB,CAAC;AAKnG,mFAAmF;AACnF,MAAM,UAAU,0BAA0B,CAAC,IAK1C;IACC,OAAO,KAAK,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE;QAC3B,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC;QAC7B,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC;QAClE,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,MAAM,CAAC,iBAAiB,EAAE,mCAAmC,OAAO,EAAE,CAAC,CAAC;QACjF,CAAC;QACD,OAAO,EAAE,CAAC,iBAAiB,CAAC,KAAK,EAAE,EAAE,GAAG,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IACpJ,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FleetId } from '@aeolus-fleet/common';
|
|
2
|
+
import type { Health } from '../shared/health.js';
|
|
2
3
|
import { type FleetLimit, type InstallationSettings } from './limits.js';
|
|
3
4
|
import type { FleetHealth, InstallationFleetFacts, InstallationFleetWindow } from './ports.js';
|
|
4
5
|
/** How far back an installation's fleet counts its messages, and over how many UTC days. */
|
|
@@ -27,11 +28,13 @@ export interface InstallationFleet {
|
|
|
27
28
|
ships: FleetLimit;
|
|
28
29
|
dailyMessages: FleetLimit;
|
|
29
30
|
};
|
|
30
|
-
|
|
31
|
+
/** Its 0041 figures, and where its deliveries are stuck (decision 0042). */
|
|
32
|
+
health: FleetHealth & Health;
|
|
31
33
|
}
|
|
32
34
|
/** What a read at `now` counts over: the last 7 times 24 hours, and the last 7 UTC days from the first one's start. */
|
|
33
35
|
export declare function messageWindow(now: Date): InstallationFleetWindow;
|
|
34
36
|
export declare function installationFleet(facts: InstallationFleetFacts, read: {
|
|
35
37
|
now: Date;
|
|
36
38
|
installation: InstallationSettings;
|
|
39
|
+
health: Health;
|
|
37
40
|
}): InstallationFleet;
|
|
@@ -14,9 +14,9 @@ export function messageWindow(now) {
|
|
|
14
14
|
return { since: new Date(now.getTime() - MESSAGE_WINDOW_MS), firstDay: new Date(`${firstDay}T00:00:00.000Z`) };
|
|
15
15
|
}
|
|
16
16
|
export function installationFleet(facts, read) {
|
|
17
|
-
const { messagesSince, messagesPerUtcDay, limitSettings, ...rest } = facts;
|
|
17
|
+
const { messagesSince, messagesPerUtcDay, limitSettings, health, ...rest } = facts;
|
|
18
18
|
const messagesPerDay = utcDays(read.now).map((date) => ({ date, count: messagesPerUtcDay.find((day) => day.date === date)?.count ?? 0 }));
|
|
19
19
|
const { ships, dailyMessages } = fleetLimitsOf({ fleetId: facts.fleetId, settings: limitSettings, installation: read.installation });
|
|
20
|
-
return { ...rest, messagesLast7Days: messagesSince, messagesToday: messagesPerDay.at(-1)?.count ?? 0, messagesPerDay, limits: { ships, dailyMessages } };
|
|
20
|
+
return { ...rest, health: { ...health, ...read.health }, messagesLast7Days: messagesSince, messagesToday: messagesPerDay.at(-1)?.count ?? 0, messagesPerDay, limits: { ships, dailyMessages } };
|
|
21
21
|
}
|
|
22
22
|
//# sourceMappingURL=installation-fleet.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"installation-fleet.js","sourceRoot":"","sources":["../../../src/domain/registry/installation-fleet.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"installation-fleet.js","sourceRoot":"","sources":["../../../src/domain/registry/installation-fleet.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAA8C,MAAM,aAAa,CAAC;AAGxF,4FAA4F;AAC5F,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC;AACrC,MAAM,MAAM,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AACnC,MAAM,CAAC,MAAM,iBAAiB,GAAG,mBAAmB,GAAG,MAAM,CAAC;AAuB9D,gGAAgG;AAChG,SAAS,OAAO,CAAC,GAAS;IACxB,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,cAAc,EAAE,EAAE,GAAG,CAAC,WAAW,EAAE,EAAE,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC;IAClF,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,mBAAmB,EAAE,EAAE,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,KAAK,GAAG,CAAC,mBAAmB,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;AAClK,CAAC;AAED,uHAAuH;AACvH,MAAM,UAAU,aAAa,CAAC,GAAS;IACrC,MAAM,CAAC,QAAQ,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IACjE,OAAO,EAAE,KAAK,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,iBAAiB,CAAC,EAAE,QAAQ,EAAE,IAAI,IAAI,CAAC,GAAG,QAAQ,gBAAgB,CAAC,EAAE,CAAC;AACjH,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,KAA6B,EAAE,IAAuE;IACtI,MAAM,EAAE,aAAa,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,KAAK,CAAC;IACnF,MAAM,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,iBAAiB,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,KAAK,IAAI,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;IAC1I,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,aAAa,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC;IACrI,OAAO,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE,iBAAiB,EAAE,aAAa,EAAE,aAAa,EAAE,cAAc,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,cAAc,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE,CAAC;AAClM,CAAC"}
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import type { Clock } from '../shared/clock.js';
|
|
2
|
+
import { type HealthRecords } from '../shared/health.js';
|
|
2
3
|
import { type InstallationFleet } from './installation-fleet.js';
|
|
3
4
|
import type { InstallationFleets, InstallationSettingsRepository } from './ports.js';
|
|
4
5
|
export type ListInstallationFleets = () => Promise<InstallationFleet[]>;
|
|
5
6
|
/**
|
|
6
7
|
* Use case: every fleet the installation hosts, oldest first, with its
|
|
7
8
|
* operator, its measures, its messages per UTC day of the last 7 days and the
|
|
8
|
-
* limits that apply to it (docs/blueprint.md, "Installation").
|
|
9
|
-
* fleets, so only the installation token reaches it.
|
|
9
|
+
* limits that apply to it and its health (docs/blueprint.md, "Installation").
|
|
10
|
+
* It reads across fleets, so only the installation token reaches it.
|
|
10
11
|
*/
|
|
11
12
|
export declare function createListInstallationFleets(deps: {
|
|
12
13
|
fleets: InstallationFleets;
|
|
13
14
|
settings: InstallationSettingsRepository;
|
|
15
|
+
health: HealthRecords;
|
|
14
16
|
clock: Clock;
|
|
15
17
|
}): ListInstallationFleets;
|
|
@@ -1,15 +1,17 @@
|
|
|
1
|
+
import { readHealthOf } from '../shared/health.js';
|
|
1
2
|
import { installationFleet, messageWindow } from './installation-fleet.js';
|
|
2
3
|
/**
|
|
3
4
|
* Use case: every fleet the installation hosts, oldest first, with its
|
|
4
5
|
* operator, its measures, its messages per UTC day of the last 7 days and the
|
|
5
|
-
* limits that apply to it (docs/blueprint.md, "Installation").
|
|
6
|
-
* fleets, so only the installation token reaches it.
|
|
6
|
+
* limits that apply to it and its health (docs/blueprint.md, "Installation").
|
|
7
|
+
* It reads across fleets, so only the installation token reaches it.
|
|
7
8
|
*/
|
|
8
9
|
export function createListInstallationFleets(deps) {
|
|
9
10
|
return async () => {
|
|
10
11
|
const now = deps.clock.now();
|
|
11
12
|
const installation = await deps.settings.read();
|
|
12
|
-
|
|
13
|
+
const fleets = await deps.fleets.list(messageWindow(now));
|
|
14
|
+
return Promise.all(fleets.map(async (facts) => installationFleet(facts, { now, installation, health: await readHealthOf(deps.health, { fleetId: facts.fleetId, now }) })));
|
|
13
15
|
};
|
|
14
16
|
}
|
|
15
17
|
//# sourceMappingURL=list-installation-fleets.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"list-installation-fleets.js","sourceRoot":"","sources":["../../../src/domain/registry/list-installation-fleets.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAA0B,MAAM,yBAAyB,CAAC;AAKnG;;;;;GAKG;AACH,MAAM,UAAU,4BAA4B,CAAC,
|
|
1
|
+
{"version":3,"file":"list-installation-fleets.js","sourceRoot":"","sources":["../../../src/domain/registry/list-installation-fleets.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAsB,MAAM,qBAAqB,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAA0B,MAAM,yBAAyB,CAAC;AAKnG;;;;;GAKG;AACH,MAAM,UAAU,4BAA4B,CAAC,IAK5C;IACC,OAAO,KAAK,IAAI,EAAE;QAChB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC;QAC7B,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QAChD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC;QAC1D,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,iBAAiB,CAAC,KAAK,EAAE,EAAE,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC7K,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A fleet's health (decision 0042): where its deliveries are stuck now, with
|
|
3
|
+
* the ship or type and the machine behind each ship, how many were stuck at
|
|
4
|
+
* each hour of the last 7 days, and how many were acknowledged in the last 24
|
|
5
|
+
* hours. Facts read from the fleet's records, never stored.
|
|
6
|
+
*/
|
|
7
|
+
import { type FleetId, type ShipId } from '@aeolus-fleet/common';
|
|
8
|
+
import type { HistoryParty } from './history.js';
|
|
9
|
+
import { type FleetRecords, type StuckPlaceCount } from './stuck.js';
|
|
10
|
+
/** A machine: the trierarch a ship's crew request is assigned to, and when its crew last called. */
|
|
11
|
+
export interface Machine extends HistoryParty {
|
|
12
|
+
lastSeenAt: Date | null;
|
|
13
|
+
}
|
|
14
|
+
/** A delivery acknowledged in the window: when its message was sent and when it was acknowledged. */
|
|
15
|
+
export interface AcknowledgedDelivery {
|
|
16
|
+
sentAt: Date;
|
|
17
|
+
acknowledgedAt: Date;
|
|
18
|
+
}
|
|
19
|
+
/** The deliveries acknowledged in the last 24 hours, the median from send to acknowledgement, and how many in each hour. */
|
|
20
|
+
export interface Acknowledged {
|
|
21
|
+
count: number;
|
|
22
|
+
/** None when nothing was acknowledged. */
|
|
23
|
+
medianMs: number | null;
|
|
24
|
+
/** Oldest first, each hour ending at `at`, the last one now. */
|
|
25
|
+
perHour: {
|
|
26
|
+
at: Date;
|
|
27
|
+
count: number;
|
|
28
|
+
}[];
|
|
29
|
+
}
|
|
30
|
+
/** What the health reads: the stuck rule's records, with the machine behind each ship whose crew request is assigned. */
|
|
31
|
+
export interface HealthFacts extends FleetRecords {
|
|
32
|
+
machines: readonly {
|
|
33
|
+
shipId: ShipId;
|
|
34
|
+
machine: Machine;
|
|
35
|
+
}[];
|
|
36
|
+
}
|
|
37
|
+
/** Outbound port: the records the health reads, always within one fleet. */
|
|
38
|
+
export interface HealthRecords {
|
|
39
|
+
/**
|
|
40
|
+
* The fleet's ships and leases not over before `since`, and its deliveries
|
|
41
|
+
* open now or with a step since then, each with every step it took.
|
|
42
|
+
*/
|
|
43
|
+
read(fleetId: FleetId, since: Date): Promise<HealthFacts>;
|
|
44
|
+
/** The fleet's deliveries acknowledged after `since`. */
|
|
45
|
+
acknowledged(fleetId: FleetId, since: Date): Promise<AcknowledgedDelivery[]>;
|
|
46
|
+
}
|
|
47
|
+
/** Where deliveries are stuck: a ship, by name, with its machine if any, or a type. */
|
|
48
|
+
export type ShownStuckPlace = {
|
|
49
|
+
kind: 'ship';
|
|
50
|
+
ship: HistoryParty;
|
|
51
|
+
machine: Machine | null;
|
|
52
|
+
} | {
|
|
53
|
+
kind: 'type';
|
|
54
|
+
type: string;
|
|
55
|
+
};
|
|
56
|
+
export interface StuckPlaceHealth extends Omit<StuckPlaceCount, 'place'> {
|
|
57
|
+
place: ShownStuckPlace;
|
|
58
|
+
}
|
|
59
|
+
export interface Health {
|
|
60
|
+
/** Stuck now, by cause and place, the oldest waiting first. */
|
|
61
|
+
stuck: StuckPlaceHealth[];
|
|
62
|
+
/** Stuck at each hour of the last 7 days, oldest first, the last one now. */
|
|
63
|
+
stuckPerHour: {
|
|
64
|
+
at: Date;
|
|
65
|
+
count: number;
|
|
66
|
+
}[];
|
|
67
|
+
/** Acknowledged in the last 24 hours. */
|
|
68
|
+
acknowledged: Acknowledged;
|
|
69
|
+
}
|
|
70
|
+
/** Since when the health reads the records for a read at `now`: the first hour of its trend. */
|
|
71
|
+
export declare function healthSince(now: Date): Date;
|
|
72
|
+
/** Since when it counts the acknowledged deliveries for a read at `now`. */
|
|
73
|
+
export declare function acknowledgedSince(now: Date): Date;
|
|
74
|
+
/** Counts the deliveries acknowledged in the 24 hours up to `now`, with their median wait and how many in each hour. */
|
|
75
|
+
export declare function acknowledgedOf(deliveries: readonly AcknowledgedDelivery[], now: Date): Acknowledged;
|
|
76
|
+
/** Reads the fleet's health at `now`: its records over the trend's hours and its acknowledged in the last 24. */
|
|
77
|
+
export declare function readHealthOf(records: HealthRecords, read: {
|
|
78
|
+
fleetId: FleetId;
|
|
79
|
+
now: Date;
|
|
80
|
+
}): Promise<Health>;
|
|
81
|
+
export declare function healthOf(facts: HealthFacts, read: {
|
|
82
|
+
acknowledged: Acknowledged;
|
|
83
|
+
now: Date;
|
|
84
|
+
}): Health;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A fleet's health (decision 0042): where its deliveries are stuck now, with
|
|
3
|
+
* the ship or type and the machine behind each ship, how many were stuck at
|
|
4
|
+
* each hour of the last 7 days, and how many were acknowledged in the last 24
|
|
5
|
+
* hours. Facts read from the fleet's records, never stored.
|
|
6
|
+
*/
|
|
7
|
+
import { ACKNOWLEDGED_HOURS, STUCK_TREND_HOURS } from '@aeolus-fleet/common';
|
|
8
|
+
import { stuckAt, stuckPerHour, stuckPlaces } from './stuck.js';
|
|
9
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
10
|
+
/** Since when the health reads the records for a read at `now`: the first hour of its trend. */
|
|
11
|
+
export function healthSince(now) {
|
|
12
|
+
return new Date(now.getTime() - STUCK_TREND_HOURS * HOUR_MS);
|
|
13
|
+
}
|
|
14
|
+
/** Since when it counts the acknowledged deliveries for a read at `now`. */
|
|
15
|
+
export function acknowledgedSince(now) {
|
|
16
|
+
return new Date(now.getTime() - ACKNOWLEDGED_HOURS * HOUR_MS);
|
|
17
|
+
}
|
|
18
|
+
/** The middle wait, or the mean of the two middle ones; none of none. */
|
|
19
|
+
function medianOf(waits) {
|
|
20
|
+
const sorted = waits.toSorted((first, second) => first - second);
|
|
21
|
+
const middle = Math.floor(sorted.length / 2);
|
|
22
|
+
const upper = sorted[middle];
|
|
23
|
+
if (upper === undefined) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const lower = sorted[middle - 1];
|
|
27
|
+
return sorted.length % 2 === 1 || lower === undefined ? upper : (lower + upper) / 2;
|
|
28
|
+
}
|
|
29
|
+
/** Counts the deliveries acknowledged in the 24 hours up to `now`, with their median wait and how many in each hour. */
|
|
30
|
+
export function acknowledgedOf(deliveries, now) {
|
|
31
|
+
const inWindow = deliveries.filter((delivery) => delivery.acknowledgedAt > acknowledgedSince(now) && delivery.acknowledgedAt <= now);
|
|
32
|
+
const perHour = Array.from({ length: ACKNOWLEDGED_HOURS }, (_unused, index) => {
|
|
33
|
+
const at = new Date(now.getTime() - (ACKNOWLEDGED_HOURS - 1 - index) * HOUR_MS);
|
|
34
|
+
const from = at.getTime() - HOUR_MS;
|
|
35
|
+
return { at, count: inWindow.filter((delivery) => delivery.acknowledgedAt.getTime() > from && delivery.acknowledgedAt <= at).length };
|
|
36
|
+
});
|
|
37
|
+
return { count: inWindow.length, medianMs: medianOf(inWindow.map((delivery) => delivery.acknowledgedAt.getTime() - delivery.sentAt.getTime())), perHour };
|
|
38
|
+
}
|
|
39
|
+
/** Reads the fleet's health at `now`: its records over the trend's hours and its acknowledged in the last 24. */
|
|
40
|
+
export async function readHealthOf(records, read) {
|
|
41
|
+
const [facts, acknowledged] = await Promise.all([records.read(read.fleetId, healthSince(read.now)), records.acknowledged(read.fleetId, acknowledgedSince(read.now))]);
|
|
42
|
+
return healthOf(facts, { acknowledged: acknowledgedOf(acknowledged, read.now), now: read.now });
|
|
43
|
+
}
|
|
44
|
+
export function healthOf(facts, read) {
|
|
45
|
+
const shown = (place) => {
|
|
46
|
+
if (place.kind === 'type') {
|
|
47
|
+
return place;
|
|
48
|
+
}
|
|
49
|
+
const name = facts.ships.find((ship) => ship.shipId === place.shipId)?.name ?? place.shipId;
|
|
50
|
+
const machine = facts.machines.find((assigned) => assigned.shipId === place.shipId)?.machine ?? null;
|
|
51
|
+
return { kind: 'ship', ship: { id: place.shipId, name }, machine };
|
|
52
|
+
};
|
|
53
|
+
return {
|
|
54
|
+
stuck: stuckPlaces(stuckAt(facts, read.now)).map(({ place, ...counted }) => ({ ...counted, place: shown(place) })),
|
|
55
|
+
stuckPerHour: stuckPerHour(facts, read.now),
|
|
56
|
+
acknowledged: read.acknowledged,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=health.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health.js","sourceRoot":"","sources":["../../../src/domain/shared/health.ts"],"names":[],"mappings":"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"}
|
|
@@ -160,6 +160,10 @@ export interface NeedsAttentionEntry {
|
|
|
160
160
|
isPushed: boolean;
|
|
161
161
|
/** When it became undeliverable, or was reported not handled. */
|
|
162
162
|
since: Date;
|
|
163
|
+
/** Why its crew reported it not handled, kept with DeliveryNotHandled (#847); null when undeliverable, or when the crew gave none. */
|
|
164
|
+
reason: string | null;
|
|
165
|
+
/** The ship whose attempt made it undeliverable, or whose turn did not handle it. */
|
|
166
|
+
ship: HistoryParty;
|
|
163
167
|
message: Omit<HistoryMessage, 'delivery'>;
|
|
164
168
|
}
|
|
165
169
|
/** The delivery states Needs attention lists, each waiting for the operator to resend or dismiss it. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"history.js","sourceRoot":"","sources":["../../../src/domain/shared/history.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,iBAAiB,GAWlB,MAAM,sBAAsB,CAAC;AAwC9B,MAAM,UAAU,QAAQ,CAAoC,OAAU;IACpE,OAAO,EAAE,GAAG,OAAO,EAAE,MAAM,EAAE,iBAAiB,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;AACxE,CAAC;AAsCD,MAAM,UAAU,oBAAoB,CAAC,IAAe;IAClD,OAAO,sBAAsB,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC;AAClE,CAAC;
|
|
1
|
+
{"version":3,"file":"history.js","sourceRoot":"","sources":["../../../src/domain/shared/history.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,iBAAiB,GAWlB,MAAM,sBAAsB,CAAC;AAwC9B,MAAM,UAAU,QAAQ,CAAoC,OAAU;IACpE,OAAO,EAAE,GAAG,OAAO,EAAE,MAAM,EAAE,iBAAiB,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;AACxE,CAAC;AAsCD,MAAM,UAAU,oBAAoB,CAAC,IAAe;IAClD,OAAO,sBAAsB,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC;AAClE,CAAC;AAqID,+FAA+F;AAC/F,MAAM,UAAU,QAAQ,CAAC,MAAmB,EAAE,KAAoB;IAChE,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,MAAM;YACT,OAAO,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,UAAU,IAAI,KAAK,KAAK,QAAQ,CAAC;QAC3E,KAAK,MAAM;YACT,OAAO,KAAK,KAAK,cAAc,CAAC;QAClC,KAAK,KAAK;YACR,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Caller } from './caller.js';
|
|
2
|
+
import type { Clock } from './clock.js';
|
|
3
|
+
import { type Health, type HealthRecords } from './health.js';
|
|
4
|
+
export type ReadHealth = (caller: Caller) => Promise<Health>;
|
|
5
|
+
/**
|
|
6
|
+
* Use case: the health of the caller's fleet (decision 0042), for the
|
|
7
|
+
* console's Health: its stuck deliveries by cause and place, the trend over
|
|
8
|
+
* the last 7 days and what was acknowledged in the last 24 hours. The caller's
|
|
9
|
+
* scope (fleet:read) is checked before this runs.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createReadHealth(deps: {
|
|
12
|
+
health: HealthRecords;
|
|
13
|
+
clock: Clock;
|
|
14
|
+
}): ReadHealth;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { readHealthOf } from './health.js';
|
|
2
|
+
/**
|
|
3
|
+
* Use case: the health of the caller's fleet (decision 0042), for the
|
|
4
|
+
* console's Health: its stuck deliveries by cause and place, the trend over
|
|
5
|
+
* the last 7 days and what was acknowledged in the last 24 hours. The caller's
|
|
6
|
+
* scope (fleet:read) is checked before this runs.
|
|
7
|
+
*/
|
|
8
|
+
export function createReadHealth(deps) {
|
|
9
|
+
return (caller) => readHealthOf(deps.health, { fleetId: caller.fleetId, now: deps.clock.now() });
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=read-health.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read-health.js","sourceRoot":"","sources":["../../../src/domain/shared/read-health.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAmC,MAAM,aAAa,CAAC;AAI5E;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAA6C;IAC5E,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;AACnG,CAAC"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stuck deliveries (decision 0042): a delivery that waits on something only a
|
|
3
|
+
* ship, a crew or the operator can change. One rule, read at any time from the
|
|
4
|
+
* fleet's records, so the deliveries stuck now and the trend replayed over 7
|
|
5
|
+
* days count alike. Facts only: no thresholds of its own beyond the clocks the
|
|
6
|
+
* pushed leases already run (decision 0040).
|
|
7
|
+
*/
|
|
8
|
+
import { type DeliveryId, type DeliveryState, type LeaseId, type MessageId, type ShipId, type StuckCause, type StuckState, type TurnState } from '@aeolus-fleet/common';
|
|
9
|
+
import type { FleetEvent } from './events.js';
|
|
10
|
+
import type { Recipient } from './selector.js';
|
|
11
|
+
/** Where a delivery is stuck: at a ship, or at the type it was sent to. */
|
|
12
|
+
export type StuckPlace = {
|
|
13
|
+
kind: 'ship';
|
|
14
|
+
shipId: ShipId;
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'type';
|
|
17
|
+
type: string;
|
|
18
|
+
};
|
|
19
|
+
export interface StuckDelivery {
|
|
20
|
+
deliveryId: DeliveryId;
|
|
21
|
+
messageId: MessageId;
|
|
22
|
+
state: StuckState;
|
|
23
|
+
cause: StuckCause;
|
|
24
|
+
place: StuckPlace;
|
|
25
|
+
/** When it entered the state it is stuck in. */
|
|
26
|
+
since: Date;
|
|
27
|
+
}
|
|
28
|
+
/** A ship as the rule reads it over time; argo and the viewer ship are always crewed. */
|
|
29
|
+
export interface ShipRecord {
|
|
30
|
+
shipId: ShipId;
|
|
31
|
+
name: string;
|
|
32
|
+
type: string;
|
|
33
|
+
isAlwaysCrewed: boolean;
|
|
34
|
+
commissionedAt: Date;
|
|
35
|
+
retiredAt: Date | null;
|
|
36
|
+
}
|
|
37
|
+
/** A turn a pushed session's hooks reported, at the time they saw it. */
|
|
38
|
+
export interface LeaseTurn {
|
|
39
|
+
state: TurnState;
|
|
40
|
+
at: Date;
|
|
41
|
+
}
|
|
42
|
+
/** A lease as the rule reads it over time: when it was held, pushed, last seen, and its turns oldest first. */
|
|
43
|
+
export interface LeaseRecord {
|
|
44
|
+
leaseId: LeaseId;
|
|
45
|
+
shipId: ShipId;
|
|
46
|
+
startedAt: Date;
|
|
47
|
+
endedAt: Date | null;
|
|
48
|
+
pushedAt: Date | null;
|
|
49
|
+
/** When its crew last called, else when it started: one column, not a history, so the trend reads it as it is now. */
|
|
50
|
+
lastSeenAt: Date;
|
|
51
|
+
turns: readonly LeaseTurn[];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A change of a delivery's state, to `state` at `at`, by the crew of
|
|
55
|
+
* `shipId` and `leaseId` when one caused it. A receipt again and a hand-over
|
|
56
|
+
* once more change no state, so they are no step.
|
|
57
|
+
*/
|
|
58
|
+
export interface DeliveryStep {
|
|
59
|
+
state: DeliveryState;
|
|
60
|
+
at: Date;
|
|
61
|
+
shipId: ShipId | null;
|
|
62
|
+
leaseId: LeaseId | null;
|
|
63
|
+
}
|
|
64
|
+
/** The step a delivery event is, or none for an event that changes no state: a receipt again, a hand-over once more, or no delivery event at all. */
|
|
65
|
+
export declare function stateStepOf(event: Pick<FleetEvent, 'type' | 'occurredAt' | 'shipId' | 'details'>): DeliveryStep | undefined;
|
|
66
|
+
/** A delivery, pending from when it was sent, then through its steps oldest first. */
|
|
67
|
+
export interface DeliveryRecord {
|
|
68
|
+
deliveryId: DeliveryId;
|
|
69
|
+
messageId: MessageId;
|
|
70
|
+
recipient: Recipient;
|
|
71
|
+
/** The ships of its type its sender could reach at send time; empty is every ship of the type (decision 0034). */
|
|
72
|
+
reachableShipIds: readonly ShipId[];
|
|
73
|
+
sentAt: Date;
|
|
74
|
+
steps: readonly DeliveryStep[];
|
|
75
|
+
}
|
|
76
|
+
/** What the rule reads: the fleet's ships, leases and deliveries over the time it is read for. */
|
|
77
|
+
export interface FleetRecords {
|
|
78
|
+
ships: readonly ShipRecord[];
|
|
79
|
+
leases: readonly LeaseRecord[];
|
|
80
|
+
deliveries: readonly DeliveryRecord[];
|
|
81
|
+
}
|
|
82
|
+
/** The deliveries stuck at `at`, each with its cause, place and since, oldest first. */
|
|
83
|
+
export declare function stuckAt(records: FleetRecords, at: Date): StuckDelivery[];
|
|
84
|
+
/** How many deliveries were stuck at each hour of the last 7 days, oldest first, the last one now. */
|
|
85
|
+
export declare function stuckPerHour(records: FleetRecords, now: Date): {
|
|
86
|
+
at: Date;
|
|
87
|
+
count: number;
|
|
88
|
+
}[];
|
|
89
|
+
/** A place deliveries are stuck in for one cause: how many, and since when the oldest waits. */
|
|
90
|
+
export interface StuckPlaceCount {
|
|
91
|
+
cause: StuckCause;
|
|
92
|
+
place: StuckPlace;
|
|
93
|
+
count: number;
|
|
94
|
+
oldestSince: Date;
|
|
95
|
+
/** Each one, oldest first. */
|
|
96
|
+
deliveries: {
|
|
97
|
+
deliveryId: DeliveryId;
|
|
98
|
+
messageId: MessageId;
|
|
99
|
+
state: StuckState;
|
|
100
|
+
since: Date;
|
|
101
|
+
}[];
|
|
102
|
+
}
|
|
103
|
+
/** The stuck deliveries grouped by cause and place, the oldest waiting first. */
|
|
104
|
+
export declare function stuckPlaces(stuck: readonly StuckDelivery[]): StuckPlaceCount[];
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stuck deliveries (decision 0042): a delivery that waits on something only a
|
|
3
|
+
* ship, a crew or the operator can change. One rule, read at any time from the
|
|
4
|
+
* fleet's records, so the deliveries stuck now and the trend replayed over 7
|
|
5
|
+
* days count alike. Facts only: no thresholds of its own beyond the clocks the
|
|
6
|
+
* pushed leases already run (decision 0040).
|
|
7
|
+
*/
|
|
8
|
+
import { isId, STUCK_TREND_HOURS } from '@aeolus-fleet/common';
|
|
9
|
+
import { hasClockRunOut, NOT_CONFIRMED_MINUTES } from '../messaging/public.js';
|
|
10
|
+
const MINUTE_MS = 60_000;
|
|
11
|
+
const HOUR_MS = 60 * MINUTE_MS;
|
|
12
|
+
/** The state each delivery event puts its delivery in. */
|
|
13
|
+
const STEP_STATES = {
|
|
14
|
+
DeliveryReceived: 'received',
|
|
15
|
+
DeliveryHanded: 'handed',
|
|
16
|
+
DeliveryReturned: 'pending',
|
|
17
|
+
DeliveryAcknowledged: 'acknowledged',
|
|
18
|
+
DeliveryHandled: 'handled',
|
|
19
|
+
DeliveryNotHandled: 'notHandled',
|
|
20
|
+
DeliveryUndeliverable: 'undeliverable',
|
|
21
|
+
DeliveryDismissed: 'dismissed',
|
|
22
|
+
DeliveryAbandoned: 'abandoned',
|
|
23
|
+
};
|
|
24
|
+
/** The step a delivery event is, or none for an event that changes no state: a receipt again, a hand-over once more, or no delivery event at all. */
|
|
25
|
+
export function stateStepOf(event) {
|
|
26
|
+
const state = STEP_STATES[event.type];
|
|
27
|
+
if (state === undefined || event.details.again === true || event.details.onceMore === true) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const { leaseId } = event.details;
|
|
31
|
+
return { state, at: event.occurredAt, shipId: event.shipId ?? null, leaseId: isId(leaseId, 'lease') ? leaseId : null };
|
|
32
|
+
}
|
|
33
|
+
function stateAt(delivery, at) {
|
|
34
|
+
if (delivery.sentAt > at) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
return delivery.steps
|
|
38
|
+
.filter((step) => step.at <= at)
|
|
39
|
+
.reduce((_state, step) => ({ state: step.state, since: step.at, shipId: step.shipId, leaseId: step.leaseId }), {
|
|
40
|
+
state: 'pending',
|
|
41
|
+
since: delivery.sentAt,
|
|
42
|
+
shipId: null,
|
|
43
|
+
leaseId: null,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function isActiveAt(ship, at) {
|
|
47
|
+
return ship.commissionedAt <= at && (ship.retiredAt === null || ship.retiredAt > at);
|
|
48
|
+
}
|
|
49
|
+
function isHeldAt(lease, at) {
|
|
50
|
+
return lease.startedAt <= at && (lease.endedAt === null || lease.endedAt > at);
|
|
51
|
+
}
|
|
52
|
+
function isPushedAt(lease, at) {
|
|
53
|
+
return lease.pushedAt !== null && lease.pushedAt <= at;
|
|
54
|
+
}
|
|
55
|
+
/** The turn its hooks had reported by then, or none yet. */
|
|
56
|
+
function turnAt(lease, at) {
|
|
57
|
+
return lease.turns.filter((turn) => turn.at <= at).at(-1) ?? null;
|
|
58
|
+
}
|
|
59
|
+
/** Reads the rule at one time over the fleet's records. */
|
|
60
|
+
class FleetAt {
|
|
61
|
+
records;
|
|
62
|
+
at;
|
|
63
|
+
constructor(records, at) {
|
|
64
|
+
this.records = records;
|
|
65
|
+
this.at = at;
|
|
66
|
+
}
|
|
67
|
+
leaseOf(shipId) {
|
|
68
|
+
return this.records.leases.find((lease) => lease.shipId === shipId && isHeldAt(lease, this.at));
|
|
69
|
+
}
|
|
70
|
+
isCrewed(ship) {
|
|
71
|
+
return ship.isAlwaysCrewed || this.leaseOf(ship.shipId) !== undefined;
|
|
72
|
+
}
|
|
73
|
+
/** The active ships that may take it: its ship, or the reachable ships of its type. */
|
|
74
|
+
takers(delivery) {
|
|
75
|
+
const { recipient } = delivery;
|
|
76
|
+
return this.records.ships.filter((ship) => {
|
|
77
|
+
if (!isActiveAt(ship, this.at)) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
if (recipient.kind === 'ship') {
|
|
81
|
+
return ship.shipId === recipient.shipId;
|
|
82
|
+
}
|
|
83
|
+
return ship.type === recipient.type && (delivery.reachableShipIds.length === 0 || delivery.reachableShipIds.includes(ship.shipId));
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/** The pushed lease whose clock ran out on a delivery waiting for it, if any. */
|
|
87
|
+
overdueOn(delivery, leases) {
|
|
88
|
+
return leases.find((lease) => isPushedAt(lease, this.at) && hasClockRunOut(delivery, { crew: { pushedAt: lease.pushedAt, turn: turnAt(lease, this.at) }, now: this.at }));
|
|
89
|
+
}
|
|
90
|
+
stuck(delivery) {
|
|
91
|
+
const now = stateAt(delivery, this.at);
|
|
92
|
+
if (now === undefined) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
const why = this.whyStuck(delivery, now);
|
|
96
|
+
return why === undefined ? undefined : { deliveryId: delivery.deliveryId, messageId: delivery.messageId, ...why, since: now.since };
|
|
97
|
+
}
|
|
98
|
+
whyStuck(delivery, now) {
|
|
99
|
+
const inState = (state, why) => (why === undefined ? undefined : { state, ...why });
|
|
100
|
+
switch (now.state) {
|
|
101
|
+
case 'undeliverable':
|
|
102
|
+
return inState('undeliverable', { cause: 'undeliverable', place: now.shipId === null ? placeOf(delivery.recipient) : { kind: 'ship', shipId: now.shipId } });
|
|
103
|
+
case 'pending':
|
|
104
|
+
return inState('pending', this.whyPendingStuck(delivery, now.since));
|
|
105
|
+
case 'received':
|
|
106
|
+
case 'handed':
|
|
107
|
+
return inState(now.state, this.whyInFlightStuck({ state: now.state, since: now.since, leaseId: now.leaseId }));
|
|
108
|
+
case 'acknowledged':
|
|
109
|
+
case 'handled':
|
|
110
|
+
case 'notHandled':
|
|
111
|
+
case 'dismissed':
|
|
112
|
+
case 'abandoned':
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
whyPendingStuck(delivery, since) {
|
|
117
|
+
const takers = this.takers(delivery);
|
|
118
|
+
if (takers.length === 0) {
|
|
119
|
+
// A direct delivery to a retired ship is abandoned, so only a type's comes here.
|
|
120
|
+
return { cause: 'noShipCanTake', place: placeOf(delivery.recipient) };
|
|
121
|
+
}
|
|
122
|
+
const crewed = takers.filter((ship) => this.isCrewed(ship));
|
|
123
|
+
if (crewed.length === 0) {
|
|
124
|
+
return { cause: 'waitingForCrew', place: placeOf(delivery.recipient) };
|
|
125
|
+
}
|
|
126
|
+
const leases = crewed.flatMap((ship) => this.leaseOf(ship.shipId) ?? []);
|
|
127
|
+
const overdue = this.overdueOn({ state: 'pending', since }, leases);
|
|
128
|
+
return overdue === undefined ? undefined : { cause: 'stalledInFlight', place: { kind: 'ship', shipId: overdue.shipId } };
|
|
129
|
+
}
|
|
130
|
+
whyInFlightStuck(delivery) {
|
|
131
|
+
const lease = this.records.leases.find((held) => held.leaseId === delivery.leaseId);
|
|
132
|
+
if (lease === undefined) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
const isStalled = isPushedAt(lease, this.at) ? this.overdueOn(delivery, [lease]) !== undefined : isQuietSinceHandOver(lease, { delivery, at: this.at });
|
|
136
|
+
return isStalled ? { cause: 'stalledInFlight', place: { kind: 'ship', shipId: lease.shipId } } : undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* A plain lease has no clock (decision 0010): its session is quiet when it was
|
|
141
|
+
* last seen no later than the hand-over, and the hand-over is as old as the
|
|
142
|
+
* pushed lease's not confirmed clock.
|
|
143
|
+
*/
|
|
144
|
+
function isQuietSinceHandOver(lease, read) {
|
|
145
|
+
const { delivery, at } = read;
|
|
146
|
+
return (delivery.state === 'handed' &&
|
|
147
|
+
lease.lastSeenAt <= delivery.since &&
|
|
148
|
+
at.getTime() - delivery.since.getTime() >= NOT_CONFIRMED_MINUTES * MINUTE_MS);
|
|
149
|
+
}
|
|
150
|
+
function placeOf(recipient) {
|
|
151
|
+
return recipient.kind === 'ship' ? { kind: 'ship', shipId: recipient.shipId } : { kind: 'type', type: recipient.type };
|
|
152
|
+
}
|
|
153
|
+
function bySince(first, second) {
|
|
154
|
+
return first.since.getTime() - second.since.getTime() || first.deliveryId.localeCompare(second.deliveryId);
|
|
155
|
+
}
|
|
156
|
+
/** The deliveries stuck at `at`, each with its cause, place and since, oldest first. */
|
|
157
|
+
export function stuckAt(records, at) {
|
|
158
|
+
const fleet = new FleetAt(records, at);
|
|
159
|
+
return records.deliveries.flatMap((delivery) => fleet.stuck(delivery) ?? []).toSorted(bySince);
|
|
160
|
+
}
|
|
161
|
+
/** How many deliveries were stuck at each hour of the last 7 days, oldest first, the last one now. */
|
|
162
|
+
export function stuckPerHour(records, now) {
|
|
163
|
+
return Array.from({ length: STUCK_TREND_HOURS }, (_unused, index) => {
|
|
164
|
+
const at = new Date(now.getTime() - (STUCK_TREND_HOURS - 1 - index) * HOUR_MS);
|
|
165
|
+
return { at, count: stuckAt(records, at).length };
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
function isSamePlace(first, second) {
|
|
169
|
+
return first.kind === 'ship' ? second.kind === 'ship' && first.shipId === second.shipId : second.kind === 'type' && first.type === second.type;
|
|
170
|
+
}
|
|
171
|
+
/** The stuck deliveries grouped by cause and place, the oldest waiting first. */
|
|
172
|
+
export function stuckPlaces(stuck) {
|
|
173
|
+
const places = [];
|
|
174
|
+
for (const delivery of stuck.toSorted(bySince)) {
|
|
175
|
+
const index = places.findIndex((counted) => counted.cause === delivery.cause && isSamePlace(counted.place, delivery.place));
|
|
176
|
+
const counted = places[index];
|
|
177
|
+
const one = { deliveryId: delivery.deliveryId, messageId: delivery.messageId, state: delivery.state, since: delivery.since };
|
|
178
|
+
if (counted === undefined) {
|
|
179
|
+
places.push({ cause: delivery.cause, place: delivery.place, count: 1, oldestSince: delivery.since, deliveries: [one] });
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
places[index] = { ...counted, count: counted.count + 1, deliveries: [...counted.deliveries, one] };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return places;
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=stuck.js.map
|