1import * as iot from "@distilled.cloud/aws/iot";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import { isResolved } from "../../Diff.ts";
5import { createPhysicalName } from "../../PhysicalName.ts";
6import * as Provider from "../../Provider.ts";
7import { Resource } from "../../Resource.ts";
8import type { Providers } from "../Providers.ts";
9
10export interface ThingProps {
11 /**
12 * Name of the thing. Must be unique in your account and region and may only
13 * contain letters, numbers, colons, underscores, and hyphens.
14 * If omitted, a unique name is generated. Changing it replaces the thing.
15 */
16 thingName?: string;
17
18 /**
19 * Name of the {@link ThingType} to associate with this thing.
20 */
21 thingTypeName?: string;
22
23 /**
24 * A set of string attributes (key/value pairs) to store on the thing.
25 */
26 attributes?: Record<string, string>;
27}
28
29export interface Thing extends Resource<
30 "AWS.IoT.Thing",
31 ThingProps,
32 {
33 /** The name of the thing. */
34 thingName: string;
35 /** The ARN of the thing. */
36 thingArn: string;
37 },
38 never,
39 Providers
40> {}
41
42/**
43 * An AWS IoT Thing — the cloud representation of a physical device.
44 *
45 * ### Creating a Thing
46 * **Example:** Basic Thing
47 * ```typescript
48 * const thing = yield* Thing("sensor", {});
49 * ```
50 *
51 * **Example:** Thing with Attributes
52 * ```typescript
53 * const thing = yield* Thing("sensor", {
54 * thingName: "temperature-sensor-01",
55 * attributes: { location: "warehouse-a", model: "acme-t1000" },
56 * });
57 * ```
58 *
59 * @resource
60 */
61export const Thing = Resource<Thing>("AWS.IoT.Thing");
62
63export const ThingProvider = () =>
64 Provider.effect(
65 Thing,
66 Effect.gen(function* () {
67 const createName = Effect.fn(function* (id: string, props: ThingProps) {
68 return props.thingName ?? (yield* createPhysicalName({ id }));
69 });
70
71 return Thing.Provider.of({
72 stables: ["thingName", "thingArn"],
73 list: () =>
74 iot.listThings.pages({}).pipe(
75 Stream.runCollect,
76 Effect.map((chunk) =>
77 Array.from(chunk).flatMap((page) =>
78 (page.things ?? [])
79 .filter((t) => t.thingName != null && t.thingArn != null)
80 .map((t) => ({
81 thingName: t.thingName!,
82 thingArn: t.thingArn!,
83 })),
84 ),
85 ),
86 ),
87 read: Effect.fn(function* ({ id, olds, output }) {
88 const thingName =
89 output?.thingName ?? (yield* createName(id, olds ?? {}));
90 const found = yield* iot
91 .describeThing({ thingName })
92 .pipe(
93 Effect.catchTag("ResourceNotFoundException", () =>
94 Effect.succeed(undefined),
95 ),
96 );
97 if (!found) return undefined;
98 return {
99 thingName,
100 thingArn: found.thingArn!,
101 };
102 }),
103 diff: Effect.fn(function* ({ id, news = {}, olds = {} }) {
104 if (!isResolved(news)) return undefined;
105 const oldName = yield* createName(id, olds);
106 const newName = yield* createName(id, news);
107 if (oldName !== newName) return { action: "replace" } as const;
108 return undefined;
109 }),
110 reconcile: Effect.fn(function* ({ id, news = {}, output, session }) {
111 const thingName = output?.thingName ?? (yield* createName(id, news));
112
113 // 1. OBSERVE — cloud is authoritative; output is only an id cache.
114 let live = yield* iot
115 .describeThing({ thingName })
116 .pipe(
117 Effect.catchTag("ResourceNotFoundException", () =>
118 Effect.succeed(undefined),
119 ),
120 );
121
122 // 2. ENSURE — create if missing; tolerate an AlreadyExists race.
123 if (live === undefined) {
124 yield* iot
125 .createThing({
126 thingName,
127 thingTypeName: news.thingTypeName,
128 attributePayload: news.attributes
129 ? { attributes: news.attributes }
130 : undefined,
131 })
132 .pipe(
133 Effect.catchTag(
134 "ResourceAlreadyExistsException",
135 () => Effect.void,
136 ),
137 );
138 live = yield* iot.describeThing({ thingName });
139 } else {
140 // 3. SYNC — converge attributes + thing type to desired.
141 const desiredAttrs = news.attributes ?? {};
142 const observedAttrs = live.attributes ?? {};
143 const attrsChanged =
144 JSON.stringify(desiredAttrs) !== JSON.stringify(observedAttrs);
145 const typeChanged =
146 (news.thingTypeName ?? undefined) !==
147 (live.thingTypeName ?? undefined);
148 if (attrsChanged || typeChanged) {
149 yield* iot.updateThing({
150 thingName,
151 thingTypeName: news.thingTypeName,
152 removeThingType:
153 news.thingTypeName === undefined &&
154 live.thingTypeName !== undefined
155 ? true
156 : undefined,
157 attributePayload: attrsChanged
158 ? { attributes: desiredAttrs, merge: false }
159 : undefined,
160 });
161 live = yield* iot.describeThing({ thingName });
162 }
163 }
164
165 yield* session.note(thingName);
166 return {
167 thingName,
168 thingArn: live.thingArn!,
169 };
170 }),
171 delete: Effect.fn(function* ({ output }) {
172 yield* iot
173 .deleteThing({ thingName: output.thingName })
174 .pipe(
175 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
176 );
177 }),
178 });
179 }),
180 );