1import * as iotfleetwise from "@distilled.cloud/aws/iotfleetwise";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import { Unowned } from "../../AdoptPolicy.ts";
5import { isResolved } from "../../Diff.ts";
6import { createPhysicalName } from "../../PhysicalName.ts";
7import * as Provider from "../../Provider.ts";
8import { Resource } from "../../Resource.ts";
9import { createInternalTags, hasAlchemyTags } from "../../Tags.ts";
10import type { Providers } from "../Providers.ts";
11import {
12 inFleetWiseRegion,
13 readFleetWiseTags,
14 retryObservation,
15 syncFleetWiseTags,
16 toFleetWiseTagList,
17} from "./internal.ts";
18
19export interface FleetProps {
20 /**
21 * ID of the fleet. Must be 1-100 characters of `[a-zA-Z0-9:_-]`. If
22 * omitted, a deterministic physical name is generated. Changing the ID
23 * replaces the fleet.
24 */
25 fleetId?: string;
26 /**
27 * ARN of the {@link SignalCatalog} the fleet is associated with.
28 * Changing the signal catalog replaces the fleet.
29 */
30 signalCatalogArn: string;
31 /**
32 * Human-readable description of the fleet.
33 */
34 description?: string;
35 /**
36 * User-defined tags for the fleet.
37 */
38 tags?: Record<string, string>;
39}
40
41export interface Fleet extends Resource<
42 "AWS.IoTFleetWise.Fleet",
43 FleetProps,
44 {
45 /** The unique ID of the fleet. */
46 fleetId: string;
47 /** The ARN of the fleet. */
48 fleetArn: string;
49 /** The signal catalog associated with the fleet. */
50 signalCatalogArn: string;
51 },
52 never,
53 Providers
54> {}
55
56/**
57 * An AWS IoT FleetWise fleet — a group of vehicles that campaigns can
58 * target collectively.
59 *
60 * Fleets are free, provisioned near-instantly, and only carry a
61 * description besides their signal-catalog association. AWS IoT FleetWise
62 * is allowlist-gated and offered in `us-east-1`/`eu-central-1` only.
63 * ### Creating a Fleet
64 * **Example:** Basic Fleet
65 * ```typescript
66 * const fleet = yield* Fleet("TestFleet", {
67 * signalCatalogArn: catalog.signalCatalogArn,
68 * description: "west-coast pilot vehicles",
69 * });
70 * ```
71 *
72 * @resource
73 */
74export const Fleet = Resource<Fleet>("AWS.IoTFleetWise.Fleet");
75
76export const FleetProvider = () =>
77 Provider.effect(
78 Fleet,
79 Effect.gen(function* () {
80 const toId = (id: string, props: { fleetId?: string }) =>
81 props.fleetId
82 ? Effect.succeed(props.fleetId)
83 : createPhysicalName({ id, maxLength: 100 });
84
85 const readFleet = Effect.fn(function* (fleetId: string) {
86 return yield* iotfleetwise.getFleet({ fleetId }).pipe(
87 inFleetWiseRegion,
88 Effect.catchTag("ResourceNotFoundException", () =>
89 Effect.succeed(undefined),
90 ),
91 );
92 });
93
94 const toAttrs = (fleet: iotfleetwise.GetFleetResponse) => ({
95 fleetId: fleet.id,
96 fleetArn: fleet.arn,
97 signalCatalogArn: fleet.signalCatalogArn,
98 });
99
100 return {
101 stables: ["fleetId", "fleetArn"],
102
103 diff: Effect.fn(function* ({ id, olds, news }) {
104 if (!isResolved(news)) return undefined;
105 if ((yield* toId(id, olds)) !== (yield* toId(id, news))) {
106 return { action: "replace" } as const;
107 }
108 // The update API can only change the description.
109 if (news.signalCatalogArn !== olds.signalCatalogArn) {
110 return { action: "replace" } as const;
111 }
112 }),
113
114 read: Effect.fn(function* ({ id, olds, output }) {
115 const fleetId = output?.fleetId ?? (yield* toId(id, olds ?? {}));
116 const found = yield* readFleet(fleetId);
117 if (found === undefined) return undefined;
118 const attrs = toAttrs(found);
119 const tags = yield* readFleetWiseTags(found.arn);
120 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
121 }),
122
123 reconcile: Effect.fn(function* ({ id, news, output, session }) {
124 const fleetId = output?.fleetId ?? (yield* toId(id, news));
125 const internalTags = yield* createInternalTags(id);
126 const desiredTags = { ...internalTags, ...news.tags };
127
128 // 1. Observe — cloud state is authoritative.
129 let observed = yield* readFleet(fleetId);
130
131 // 2. Ensure — create if missing; tolerate the AlreadyExists race.
132 if (observed === undefined) {
133 yield* iotfleetwise
134 .createFleet({
135 fleetId,
136 signalCatalogArn: news.signalCatalogArn,
137 description: news.description,
138 tags: toFleetWiseTagList(desiredTags),
139 })
140 .pipe(
141 inFleetWiseRegion,
142 Effect.catchTag("ConflictException", () => Effect.void),
143 );
144 observed = yield* readFleet(fleetId).pipe(
145 Effect.flatMap((fleet) =>
146 fleet === undefined
147 ? Effect.fail(new Error(`Fleet '${fleetId}' not found`))
148 : Effect.succeed(fleet),
149 ),
150 retryObservation,
151 );
152 }
153
154 // 3. Sync description — the only mutable aspect.
155 if (
156 news.description !== undefined &&
157 news.description !== observed.description
158 ) {
159 yield* iotfleetwise
160 .updateFleet({ fleetId, description: news.description })
161 .pipe(inFleetWiseRegion);
162 }
163
164 // 3b. Sync tags against OBSERVED cloud tags.
165 yield* syncFleetWiseTags(observed.arn, desiredTags);
166
167 yield* session.note(fleetId);
168 return toAttrs(observed);
169 }),
170
171 delete: Effect.fn(function* ({ output }) {
172 // Idempotent: deleting a missing fleet succeeds (vehicles are
173 // detached automatically before deletion).
174 yield* iotfleetwise
175 .deleteFleet({ fleetId: output.fleetId })
176 .pipe(inFleetWiseRegion);
177 }),
178
179 list: () =>
180 iotfleetwise.listFleets.items({}).pipe(
181 Stream.runCollect,
182 Effect.map((chunk) =>
183 Array.from(chunk).map((summary) => ({
184 fleetId: summary.id,
185 fleetArn: summary.arn,
186 signalCatalogArn: summary.signalCatalogArn,
187 })),
188 ),
189 inFleetWiseRegion,
190 ),
191 };
192 }),
193 );