alchemy 2.0.0-beta.64 → 2.0.0-beta.65

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.
Files changed (165) hide show
  1. package/lib/AWS/EC2/InternetGateway.d.ts.map +1 -1
  2. package/lib/AWS/EC2/InternetGateway.js +8 -3
  3. package/lib/AWS/EC2/InternetGateway.js.map +1 -1
  4. package/lib/AWS/EC2/LingeringEnis.d.ts +26 -0
  5. package/lib/AWS/EC2/LingeringEnis.d.ts.map +1 -0
  6. package/lib/AWS/EC2/LingeringEnis.js +128 -0
  7. package/lib/AWS/EC2/LingeringEnis.js.map +1 -0
  8. package/lib/AWS/EC2/SecurityGroup.d.ts.map +1 -1
  9. package/lib/AWS/EC2/SecurityGroup.js +14 -14
  10. package/lib/AWS/EC2/SecurityGroup.js.map +1 -1
  11. package/lib/AWS/EC2/Subnet.d.ts.map +1 -1
  12. package/lib/AWS/EC2/Subnet.js +13 -21
  13. package/lib/AWS/EC2/Subnet.js.map +1 -1
  14. package/lib/AWS/ECR/Image.d.ts +1 -0
  15. package/lib/AWS/ECR/Image.d.ts.map +1 -1
  16. package/lib/AWS/EKS/Addon.d.ts.map +1 -1
  17. package/lib/AWS/EKS/Addon.js +15 -3
  18. package/lib/AWS/EKS/Addon.js.map +1 -1
  19. package/lib/AWS/EKS/Deployment.d.ts +10 -1
  20. package/lib/AWS/EKS/Deployment.d.ts.map +1 -1
  21. package/lib/AWS/EKS/Deployment.js +10 -4
  22. package/lib/AWS/EKS/Deployment.js.map +1 -1
  23. package/lib/AWS/EKS/HyperPod.d.ts +57 -0
  24. package/lib/AWS/EKS/HyperPod.d.ts.map +1 -0
  25. package/lib/AWS/EKS/HyperPod.js +46 -0
  26. package/lib/AWS/EKS/HyperPod.js.map +1 -0
  27. package/lib/AWS/EKS/Job.d.ts +10 -1
  28. package/lib/AWS/EKS/Job.d.ts.map +1 -1
  29. package/lib/AWS/EKS/Job.js +16 -6
  30. package/lib/AWS/EKS/Job.js.map +1 -1
  31. package/lib/AWS/EKS/internal/client.d.ts +4 -3
  32. package/lib/AWS/EKS/internal/client.d.ts.map +1 -1
  33. package/lib/AWS/EKS/internal/client.js +33 -2
  34. package/lib/AWS/EKS/internal/client.js.map +1 -1
  35. package/lib/AWS/Lambda/Function.d.ts.map +1 -1
  36. package/lib/AWS/Lambda/Function.js +1 -1
  37. package/lib/AWS/Lambda/Function.js.map +1 -1
  38. package/lib/AWS/Providers.d.ts.map +1 -1
  39. package/lib/AWS/Providers.js +4 -1
  40. package/lib/AWS/Providers.js.map +1 -1
  41. package/lib/AWS/SageMaker/Cluster.d.ts +327 -0
  42. package/lib/AWS/SageMaker/Cluster.d.ts.map +1 -0
  43. package/lib/AWS/SageMaker/Cluster.js +469 -0
  44. package/lib/AWS/SageMaker/Cluster.js.map +1 -0
  45. package/lib/AWS/SageMaker/ClusterSchedulerConfig.d.ts +96 -0
  46. package/lib/AWS/SageMaker/ClusterSchedulerConfig.d.ts.map +1 -0
  47. package/lib/AWS/SageMaker/ClusterSchedulerConfig.js +270 -0
  48. package/lib/AWS/SageMaker/ClusterSchedulerConfig.js.map +1 -0
  49. package/lib/AWS/SageMaker/ComputeQuota.d.ts +106 -0
  50. package/lib/AWS/SageMaker/ComputeQuota.d.ts.map +1 -0
  51. package/lib/AWS/SageMaker/ComputeQuota.js +268 -0
  52. package/lib/AWS/SageMaker/ComputeQuota.js.map +1 -0
  53. package/lib/AWS/SageMaker/index.d.ts +3 -0
  54. package/lib/AWS/SageMaker/index.d.ts.map +1 -1
  55. package/lib/AWS/SageMaker/index.js +3 -0
  56. package/lib/AWS/SageMaker/index.js.map +1 -1
  57. package/lib/Bundle/PurePlugin.d.ts +29 -7
  58. package/lib/Bundle/PurePlugin.d.ts.map +1 -1
  59. package/lib/Bundle/PurePlugin.js +56 -33
  60. package/lib/Bundle/PurePlugin.js.map +1 -1
  61. package/lib/Cli/commands/nuke.d.ts.map +1 -1
  62. package/lib/Cli/commands/nuke.js +231 -37
  63. package/lib/Cli/commands/nuke.js.map +1 -1
  64. package/lib/Cloudflare/StateStore/Api.d.ts +1 -1
  65. package/lib/Cloudflare/Workers/Assets.d.ts +26 -8
  66. package/lib/Cloudflare/Workers/Assets.d.ts.map +1 -1
  67. package/lib/Cloudflare/Workers/Assets.js +46 -1
  68. package/lib/Cloudflare/Workers/Assets.js.map +1 -1
  69. package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts +2 -2
  70. package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts.map +1 -1
  71. package/lib/Cloudflare/Workers/LocalWorkerProvider.js +39 -6
  72. package/lib/Cloudflare/Workers/LocalWorkerProvider.js.map +1 -1
  73. package/lib/Cloudflare/Workers/Worker.d.ts +7 -5
  74. package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
  75. package/lib/Cloudflare/Workers/Worker.js +0 -1
  76. package/lib/Cloudflare/Workers/Worker.js.map +1 -1
  77. package/lib/Cloudflare/Workers/WorkerBinding.d.ts +2 -2
  78. package/lib/Cloudflare/Workers/WorkerBundle.d.ts +20 -1
  79. package/lib/Cloudflare/Workers/WorkerBundle.d.ts.map +1 -1
  80. package/lib/Cloudflare/Workers/WorkerBundle.js +11 -0
  81. package/lib/Cloudflare/Workers/WorkerBundle.js.map +1 -1
  82. package/lib/Cloudflare/Workers/WorkerProvider.d.ts.map +1 -1
  83. package/lib/Cloudflare/Workers/WorkerProvider.js +10 -4
  84. package/lib/Cloudflare/Workers/WorkerProvider.js.map +1 -1
  85. package/lib/Cloudflare/Workflows/WorkflowBridge.d.ts +2 -1
  86. package/lib/Cloudflare/Workflows/WorkflowBridge.d.ts.map +1 -1
  87. package/lib/Cloudflare/Workflows/WorkflowBridge.js +5 -5
  88. package/lib/Cloudflare/Workflows/WorkflowBridge.js.map +1 -1
  89. package/lib/Cloudflare/Workflows/index.d.ts +1 -1
  90. package/lib/Cloudflare/Workflows/index.d.ts.map +1 -1
  91. package/lib/Cloudflare/Workflows/index.js +1 -1
  92. package/lib/Cloudflare/Workflows/index.js.map +1 -1
  93. package/lib/Docker/Container.d.ts +26 -0
  94. package/lib/Docker/Container.d.ts.map +1 -1
  95. package/lib/Docker/Container.js +20 -1
  96. package/lib/Docker/Container.js.map +1 -1
  97. package/lib/Docker/Docker.d.ts +2 -0
  98. package/lib/Docker/Docker.d.ts.map +1 -1
  99. package/lib/Docker/Docker.js.map +1 -1
  100. package/lib/Output.d.ts +4 -1
  101. package/lib/Output.d.ts.map +1 -1
  102. package/lib/Output.js.map +1 -1
  103. package/lib/Provider.d.ts +20 -0
  104. package/lib/Provider.d.ts.map +1 -1
  105. package/lib/Provider.js.map +1 -1
  106. package/lib/Random.d.ts +1 -1
  107. package/lib/Random.d.ts.map +1 -1
  108. package/lib/Resource.d.ts +14 -1
  109. package/lib/Resource.d.ts.map +1 -1
  110. package/lib/Resource.js.map +1 -1
  111. package/lib/SQL/D1.d.ts +1 -1
  112. package/lib/SQL/D1.js +1 -1
  113. package/lib/SQL/Postgres.d.ts +1 -1
  114. package/lib/SQL/Postgres.js +1 -1
  115. package/lib/State/HttpStateApi.d.ts +11 -11
  116. package/lib/Test/Alchemy.d.ts +1 -1
  117. package/lib/Test/Alchemy.js +1 -1
  118. package/lib/Test/Vitest.d.ts +58 -0
  119. package/lib/Test/Vitest.d.ts.map +1 -0
  120. package/lib/Test/Vitest.js +114 -0
  121. package/lib/Test/Vitest.js.map +1 -0
  122. package/lib/Util/proxy-chain.d.ts +5 -0
  123. package/lib/Util/proxy-chain.d.ts.map +1 -1
  124. package/lib/Util/proxy-chain.js +52 -4
  125. package/lib/Util/proxy-chain.js.map +1 -1
  126. package/package.json +23 -15
  127. package/src/AWS/EC2/InternetGateway.ts +8 -3
  128. package/src/AWS/EC2/LingeringEnis.ts +169 -0
  129. package/src/AWS/EC2/SecurityGroup.ts +24 -28
  130. package/src/AWS/EC2/Subnet.ts +23 -34
  131. package/src/AWS/EKS/Addon.ts +17 -4
  132. package/src/AWS/EKS/Deployment.ts +26 -5
  133. package/src/AWS/EKS/HyperPod.ts +97 -0
  134. package/src/AWS/EKS/Job.ts +32 -7
  135. package/src/AWS/EKS/internal/client.ts +40 -3
  136. package/src/AWS/Lambda/Function.ts +5 -3
  137. package/src/AWS/Providers.ts +6 -0
  138. package/src/AWS/SageMaker/Cluster.ts +775 -0
  139. package/src/AWS/SageMaker/ClusterSchedulerConfig.ts +430 -0
  140. package/src/AWS/SageMaker/ComputeQuota.ts +426 -0
  141. package/src/AWS/SageMaker/index.ts +3 -0
  142. package/src/Bundle/PurePlugin.ts +92 -39
  143. package/src/Cli/commands/nuke.ts +295 -56
  144. package/src/Cloudflare/Workers/Assets.ts +53 -1
  145. package/src/Cloudflare/Workers/LocalWorkerProvider.ts +46 -7
  146. package/src/Cloudflare/Workers/Worker.ts +7 -5
  147. package/src/Cloudflare/Workers/WorkerBundle.ts +31 -1
  148. package/src/Cloudflare/Workers/WorkerProvider.ts +18 -4
  149. package/src/Cloudflare/Workflows/WorkflowBridge.ts +5 -5
  150. package/src/Cloudflare/Workflows/index.ts +1 -1
  151. package/src/Docker/Container.ts +30 -1
  152. package/src/Docker/Docker.ts +2 -0
  153. package/src/Output.ts +19 -11
  154. package/src/Provider.ts +20 -0
  155. package/src/Resource.ts +18 -1
  156. package/src/SQL/D1.ts +1 -1
  157. package/src/SQL/Postgres.ts +1 -1
  158. package/src/Test/Alchemy.ts +1 -1
  159. package/src/Test/Vitest.ts +244 -0
  160. package/src/Util/proxy-chain.ts +73 -4
  161. package/lib/SQL/index.d.ts +0 -4
  162. package/lib/SQL/index.d.ts.map +0 -1
  163. package/lib/SQL/index.js +0 -4
  164. package/lib/SQL/index.js.map +0 -1
  165. package/src/SQL/index.ts +0 -3
@@ -0,0 +1,426 @@
1
+ import * as sagemaker from "@distilled.cloud/aws/sagemaker";
2
+ import * as Data from "effect/Data";
3
+ import * as Effect from "effect/Effect";
4
+ import * as Schedule from "effect/Schedule";
5
+ import * as EffectStream from "effect/Stream";
6
+ import { Unowned } from "../../AdoptPolicy.ts";
7
+ import { isResolved } from "../../Diff.ts";
8
+ import { createPhysicalName } from "../../PhysicalName.ts";
9
+ import * as Provider from "../../Provider.ts";
10
+ import { Resource } from "../../Resource.ts";
11
+ import {
12
+ createInternalTags,
13
+ diffTags,
14
+ hasAlchemyTags,
15
+ type Tags,
16
+ } from "../../Tags.ts";
17
+ import type { Providers } from "../Providers.ts";
18
+
19
+ export interface ComputeQuotaProps {
20
+ /**
21
+ * Name of the compute allocation. Maximum 63 characters, alphanumeric and
22
+ * hyphens.
23
+ * @default ${app}-${stage}-${id}
24
+ */
25
+ name?: string;
26
+ /**
27
+ * ARN of the EKS-orchestrated HyperPod cluster the allocation applies to.
28
+ * Changing the cluster replaces the allocation.
29
+ */
30
+ clusterArn: string;
31
+ /**
32
+ * The team the quota is allocated to (its name maps to a Kubernetes
33
+ * namespace `hyperpod-ns-<team-name>`) and its fair-share weight.
34
+ */
35
+ computeQuotaTarget: sagemaker.ComputeQuotaTarget;
36
+ /**
37
+ * The allocation itself: instance-type quotas, the resource-sharing
38
+ * strategy for idle compute, and whether the team's own tasks can preempt
39
+ * each other.
40
+ */
41
+ computeQuotaConfig?: sagemaker.ComputeQuotaConfig;
42
+ /**
43
+ * Whether the quota is enforced.
44
+ * @default "Enabled"
45
+ */
46
+ activationState?: sagemaker.ActivationState;
47
+ /**
48
+ * A description of the compute allocation.
49
+ */
50
+ description?: string;
51
+ /**
52
+ * Tags to associate with the compute allocation. Merged with internal
53
+ * Alchemy tags.
54
+ */
55
+ tags?: Record<string, string>;
56
+ }
57
+
58
+ export interface ComputeQuota extends Resource<
59
+ "AWS.SageMaker.ComputeQuota",
60
+ ComputeQuotaProps,
61
+ {
62
+ /**
63
+ * The compute allocation's ID.
64
+ */
65
+ computeQuotaId: string;
66
+ /**
67
+ * ARN of the compute allocation.
68
+ */
69
+ computeQuotaArn: string;
70
+ /**
71
+ * The compute allocation's name.
72
+ */
73
+ name: string;
74
+ /**
75
+ * ARN of the HyperPod cluster the allocation applies to.
76
+ */
77
+ clusterArn: string;
78
+ /**
79
+ * The allocation's current version (incremented on every update).
80
+ */
81
+ computeQuotaVersion: number;
82
+ /**
83
+ * The team the quota is allocated to. Task governance materializes the
84
+ * `hyperpod-ns-<teamName>` namespace and its Kueue LocalQueue from it.
85
+ */
86
+ teamName: string;
87
+ },
88
+ never,
89
+ Providers
90
+ > {}
91
+
92
+ /**
93
+ * A SageMaker HyperPod compute allocation (task governance) — reserves
94
+ * instance capacity on an EKS-orchestrated HyperPod cluster for a team,
95
+ * with fair-share weights and borrow/lend rules for idle compute.
96
+ * @resource
97
+ * @section Creating Compute Allocations
98
+ * @example Team Quota with Borrowing
99
+ * ```typescript
100
+ * import * as AWS from "alchemy/AWS";
101
+ *
102
+ * const quota = yield* AWS.SageMaker.ComputeQuota("ResearchQuota", {
103
+ * clusterArn: hyperpod.clusterArn,
104
+ * computeQuotaTarget: { TeamName: "research", FairShareWeight: 10 },
105
+ * computeQuotaConfig: {
106
+ * ComputeQuotaResources: [{ InstanceType: "ml.g5.xlarge", Count: 2 }],
107
+ * ResourceSharingConfig: { Strategy: "Lend", BorrowLimit: 50 },
108
+ * },
109
+ * });
110
+ * ```
111
+ */
112
+ export const ComputeQuota = Resource<ComputeQuota>(
113
+ "AWS.SageMaker.ComputeQuota",
114
+ );
115
+
116
+ const createQuotaName = (id: string, props: { name?: string | undefined }) =>
117
+ props.name
118
+ ? Effect.succeed(props.name)
119
+ : createPhysicalName({ id, maxLength: 63 });
120
+
121
+ const describeQuotaOrUndefined = (quotaId: string) =>
122
+ sagemaker
123
+ .describeComputeQuota({ ComputeQuotaId: quotaId })
124
+ .pipe(Effect.catchTag("ResourceNotFound", () => Effect.succeed(undefined)));
125
+
126
+ /**
127
+ * Look a compute allocation up by exact name — used when state was lost
128
+ * (read without output) or a create raced.
129
+ */
130
+ const findQuotaByName = Effect.fn(function* (name: string) {
131
+ const summaries = yield* sagemaker.listComputeQuotas
132
+ .pages({ NameContains: name })
133
+ .pipe(
134
+ EffectStream.runCollect,
135
+ Effect.map((chunk) =>
136
+ Array.from(chunk).flatMap((page) => page.ComputeQuotaSummaries ?? []),
137
+ ),
138
+ );
139
+ return summaries.find((s) => s.Name === name && s.Status !== "Deleted");
140
+ });
141
+
142
+ const fetchQuotaTags = Effect.fn(function* (arn: string) {
143
+ const response = yield* sagemaker
144
+ .listTags({ ResourceArn: arn })
145
+ .pipe(
146
+ Effect.catchTag("AccessDeniedException", () => Effect.succeed(undefined)),
147
+ );
148
+ return Object.fromEntries(
149
+ (response?.Tags ?? []).flatMap((tag) =>
150
+ tag.Key !== undefined ? [[tag.Key, tag.Value ?? ""]] : [],
151
+ ),
152
+ );
153
+ });
154
+
155
+ const toAttrs = (
156
+ described: sagemaker.DescribeComputeQuotaResponse,
157
+ ): ComputeQuota["Attributes"] => ({
158
+ computeQuotaId: described.ComputeQuotaId,
159
+ computeQuotaArn: described.ComputeQuotaArn,
160
+ name: described.Name,
161
+ clusterArn: described.ClusterArn ?? "",
162
+ computeQuotaVersion: described.ComputeQuotaVersion,
163
+ teamName: described.ComputeQuotaTarget?.TeamName ?? "",
164
+ });
165
+
166
+ /**
167
+ * The compute allocation is still transitioning toward the awaited state —
168
+ * retried by the bounded wait schedule.
169
+ */
170
+ class ComputeQuotaNotReady extends Data.TaggedError("ComputeQuotaNotReady")<{
171
+ readonly quotaId: string;
172
+ readonly status: string | undefined;
173
+ }> {}
174
+
175
+ /**
176
+ * The compute allocation converged to a terminal failed status.
177
+ */
178
+ export class ComputeQuotaFailed extends Data.TaggedError("ComputeQuotaFailed")<{
179
+ readonly quotaId: string;
180
+ readonly status: string | undefined;
181
+ readonly message: string | undefined;
182
+ }> {}
183
+
184
+ const FAILED_STATUSES: string[] = [
185
+ "CreateFailed",
186
+ "CreateRollbackFailed",
187
+ "UpdateFailed",
188
+ "UpdateRollbackFailed",
189
+ "DeleteFailed",
190
+ "DeleteRollbackFailed",
191
+ ];
192
+
193
+ // Explicitly-typed retry wrapper — an inline `Effect.retry` in provider
194
+ // lifecycle code leaks `Retry.Return`'s conditional type into declaration
195
+ // emit and widens the provider layer to `unknown` for every consumer of
196
+ // `AWS.providers()`.
197
+ const retryWhileNotReady = <A, E extends { readonly _tag: string }, R>(
198
+ self: Effect.Effect<A, E, R>,
199
+ ): Effect.Effect<A, E, R> =>
200
+ Effect.retry(self, {
201
+ while: (e) => e._tag === "ComputeQuotaNotReady",
202
+ schedule: Schedule.max([Schedule.spaced("5 seconds"), Schedule.recurs(60)]),
203
+ });
204
+
205
+ const waitForQuota = (quotaId: string, target: "Ready" | "Gone") =>
206
+ retryWhileNotReady(
207
+ Effect.gen(function* () {
208
+ const described = yield* describeQuotaOrUndefined(quotaId);
209
+ if (target === "Gone") {
210
+ if (described === undefined || described.Status === "Deleted") return;
211
+ if (FAILED_STATUSES.includes(described.Status)) {
212
+ return yield* Effect.fail(
213
+ new ComputeQuotaFailed({
214
+ quotaId,
215
+ status: described.Status,
216
+ message: described.FailureReason,
217
+ }),
218
+ );
219
+ }
220
+ return yield* Effect.fail(
221
+ new ComputeQuotaNotReady({ quotaId, status: described.Status }),
222
+ );
223
+ }
224
+ if (described?.Status === "Created" || described?.Status === "Updated") {
225
+ return;
226
+ }
227
+ if (
228
+ described !== undefined &&
229
+ FAILED_STATUSES.includes(described.Status)
230
+ ) {
231
+ return yield* Effect.fail(
232
+ new ComputeQuotaFailed({
233
+ quotaId,
234
+ status: described.Status,
235
+ message: described.FailureReason,
236
+ }),
237
+ );
238
+ }
239
+ return yield* Effect.fail(
240
+ new ComputeQuotaNotReady({ quotaId, status: described?.Status }),
241
+ );
242
+ }),
243
+ );
244
+
245
+ export const ComputeQuotaProvider = () =>
246
+ Provider.effect(
247
+ ComputeQuota,
248
+ Effect.gen(function* () {
249
+ return {
250
+ stables: ["computeQuotaId", "computeQuotaArn", "name", "clusterArn"],
251
+ list: () =>
252
+ Effect.gen(function* () {
253
+ const summaries = yield* sagemaker.listComputeQuotas.pages({}).pipe(
254
+ EffectStream.runCollect,
255
+ Effect.map((chunk) =>
256
+ Array.from(chunk).flatMap(
257
+ (page) => page.ComputeQuotaSummaries ?? [],
258
+ ),
259
+ ),
260
+ );
261
+ return summaries.flatMap((s) =>
262
+ s.ComputeQuotaId !== undefined && s.Status !== "Deleted"
263
+ ? [
264
+ {
265
+ computeQuotaId: s.ComputeQuotaId,
266
+ computeQuotaArn: s.ComputeQuotaArn ?? "",
267
+ name: s.Name ?? "",
268
+ clusterArn: s.ClusterArn ?? "",
269
+ computeQuotaVersion: s.ComputeQuotaVersion ?? 1,
270
+ teamName: s.ComputeQuotaTarget?.TeamName ?? "",
271
+ },
272
+ ]
273
+ : [],
274
+ );
275
+ }),
276
+ read: Effect.fn(function* ({ id, olds, output }) {
277
+ const quotaId =
278
+ output?.computeQuotaId ??
279
+ (yield* findQuotaByName(yield* createQuotaName(id, olds ?? {})))
280
+ ?.ComputeQuotaId;
281
+ if (quotaId === undefined) return undefined;
282
+ const described = yield* describeQuotaOrUndefined(quotaId);
283
+ if (
284
+ !described ||
285
+ described.Status === "Deleting" ||
286
+ described.Status === "Deleted"
287
+ ) {
288
+ return undefined;
289
+ }
290
+ const attrs = toAttrs(described);
291
+ const tags = yield* fetchQuotaTags(attrs.computeQuotaArn);
292
+ return (yield* hasAlchemyTags(id, tags as Tags))
293
+ ? attrs
294
+ : Unowned(attrs);
295
+ }),
296
+ diff: Effect.fn(function* ({ id, news, olds }) {
297
+ if (!isResolved(news)) return;
298
+ if (olds === undefined) return;
299
+ const oldName = yield* createQuotaName(id, olds);
300
+ const newName = yield* createQuotaName(id, news);
301
+ // The name and target cluster are fixed at creation.
302
+ if (oldName !== newName || olds.clusterArn !== news.clusterArn) {
303
+ return { action: "replace" } as const;
304
+ }
305
+ }),
306
+ reconcile: Effect.fn(function* ({ id, news, output, session }) {
307
+ if (!news) {
308
+ return yield* Effect.fail(
309
+ new Error("SageMaker ComputeQuota requires props"),
310
+ );
311
+ }
312
+ const name = yield* createQuotaName(id, news);
313
+ const internalTags = yield* createInternalTags(id);
314
+ const desiredTags = { ...internalTags, ...news.tags };
315
+
316
+ // Observe — by cached id first, then by name (lost state or race).
317
+ let quotaId = output?.computeQuotaId;
318
+ let described =
319
+ quotaId !== undefined
320
+ ? yield* describeQuotaOrUndefined(quotaId)
321
+ : undefined;
322
+ if (described === undefined) {
323
+ const found = yield* findQuotaByName(name);
324
+ described =
325
+ found?.ComputeQuotaId !== undefined
326
+ ? yield* describeQuotaOrUndefined(found.ComputeQuotaId)
327
+ : undefined;
328
+ }
329
+
330
+ // Ensure — create if missing; a Conflict means a concurrent
331
+ // create won the race, so re-observe by name.
332
+ if (described === undefined) {
333
+ const created = yield* sagemaker
334
+ .createComputeQuota({
335
+ Name: name,
336
+ ClusterArn: news.clusterArn,
337
+ ComputeQuotaConfig: news.computeQuotaConfig,
338
+ ComputeQuotaTarget: news.computeQuotaTarget,
339
+ ActivationState: news.activationState,
340
+ Description: news.description,
341
+ Tags: Object.entries(desiredTags).map(([Key, Value]) => ({
342
+ Key,
343
+ Value,
344
+ })),
345
+ })
346
+ .pipe(
347
+ Effect.catchTag("ConflictException", () =>
348
+ Effect.succeed(undefined),
349
+ ),
350
+ );
351
+ quotaId =
352
+ created?.ComputeQuotaId ??
353
+ (yield* findQuotaByName(name))?.ComputeQuotaId;
354
+ if (quotaId === undefined) {
355
+ return yield* Effect.fail(
356
+ new Error(`failed to create compute quota ${name}`),
357
+ );
358
+ }
359
+ yield* session.note(`Creating compute quota ${name}...`);
360
+ yield* waitForQuota(quotaId, "Ready");
361
+ } else {
362
+ quotaId = described.ComputeQuotaId;
363
+ // Wait out any in-flight transition before diffing.
364
+ yield* waitForQuota(quotaId, "Ready");
365
+ described = yield* describeQuotaOrUndefined(quotaId);
366
+ // Sync — diff observed allocation against desired.
367
+ if (
368
+ described !== undefined &&
369
+ (JSON.stringify(described.ComputeQuotaConfig) !==
370
+ JSON.stringify(news.computeQuotaConfig) ||
371
+ JSON.stringify(described.ComputeQuotaTarget) !==
372
+ JSON.stringify(news.computeQuotaTarget) ||
373
+ (news.activationState !== undefined &&
374
+ described.ActivationState !== news.activationState) ||
375
+ (described.Description ?? undefined) !==
376
+ (news.description ?? undefined))
377
+ ) {
378
+ yield* sagemaker.updateComputeQuota({
379
+ ComputeQuotaId: quotaId,
380
+ TargetVersion: described.ComputeQuotaVersion,
381
+ ComputeQuotaConfig: news.computeQuotaConfig,
382
+ ComputeQuotaTarget: news.computeQuotaTarget,
383
+ ActivationState: news.activationState,
384
+ Description: news.description,
385
+ });
386
+ yield* session.note(`Updating compute quota ${name}...`);
387
+ yield* waitForQuota(quotaId, "Ready");
388
+ }
389
+ }
390
+
391
+ described = yield* describeQuotaOrUndefined(quotaId);
392
+ if (described === undefined) {
393
+ return yield* Effect.fail(
394
+ new Error(`failed to read reconciled compute quota ${name}`),
395
+ );
396
+ }
397
+ const attrs = toAttrs(described);
398
+
399
+ // Sync tags — diff against OBSERVED cloud tags.
400
+ const currentTags = yield* fetchQuotaTags(attrs.computeQuotaArn);
401
+ const { removed, upsert } = diffTags(currentTags, desiredTags);
402
+ if (removed.length > 0) {
403
+ yield* sagemaker.deleteTags({
404
+ ResourceArn: attrs.computeQuotaArn,
405
+ TagKeys: removed,
406
+ });
407
+ }
408
+ if (upsert.length > 0) {
409
+ yield* sagemaker.addTags({
410
+ ResourceArn: attrs.computeQuotaArn,
411
+ Tags: upsert.map(({ Key, Value }) => ({ Key, Value })),
412
+ });
413
+ }
414
+
415
+ yield* session.note(attrs.computeQuotaArn);
416
+ return attrs;
417
+ }),
418
+ delete: Effect.fn(function* ({ output }) {
419
+ yield* sagemaker
420
+ .deleteComputeQuota({ ComputeQuotaId: output.computeQuotaId })
421
+ .pipe(Effect.catchTag("ResourceNotFound", () => Effect.void));
422
+ yield* waitForQuota(output.computeQuotaId, "Gone");
423
+ }),
424
+ };
425
+ }),
426
+ );
@@ -1,5 +1,8 @@
1
1
  export * from "./BatchGetRecord.ts";
2
2
  export * from "./BatchGetRecordHttp.ts";
3
+ export * from "./Cluster.ts";
4
+ export * from "./ClusterSchedulerConfig.ts";
5
+ export * from "./ComputeQuota.ts";
3
6
  export * from "./BatchWriteRecord.ts";
4
7
  export * from "./BatchWriteRecordHttp.ts";
5
8
  export * from "./DeleteRecord.ts";
@@ -62,10 +62,11 @@ export interface PurePluginOptions {
62
62
  * too. This makes the user's own source tree-shakeable without any
63
63
  * configuration.
64
64
  *
65
- * The package's `sideEffects` field is NOT consulted for this gate —
66
- * if your entry's package has init-time side effects you wish to
67
- * preserve, declare them in `package.json` (`"sideEffects": ["./foo.ts"]`)
68
- * or set this option to `false`.
65
+ * Only calls whose result is used (variable initializers, exports) are
66
+ * annotated in the auto-detected package. Top-level calls whose result
67
+ * is discarded (e.g. Hono/Express-style `app.get("/", handler)` route
68
+ * registrations) are preserved unless the package explicitly declares
69
+ * `"sideEffects": false` (or `[]`) in its `package.json`.
69
70
  *
70
71
  * @default true
71
72
  */
@@ -139,19 +140,32 @@ export const purePlugin = (
139
140
  if (name === null || !isMatch(name)) return null;
140
141
 
141
142
  // Only override `moduleSideEffects` when the owning package
142
- // explicitly opts in via `sideEffects: false` / `[]`. Pure
143
- // annotations themselves are always safe to add (they only mean
144
- // "if the result is unused, the call may be dropped"), but
145
- // marking arbitrary modules side-effect-free could erase
146
- // intentional registrations / mutations the author made at the
147
- // top level of files in packages that did not declare so.
143
+ // explicitly opts in via `sideEffects: false` / `[]`. Marking
144
+ // arbitrary modules side-effect-free could erase intentional
145
+ // registrations / mutations the author made at the top level of
146
+ // files in packages that did not declare so.
147
+ const isEntry = entryPaths.has(cleanId);
148
+ const sideEffectFreePkg = isSideEffectFree(info?.sideEffects);
148
149
  const markSideEffectFree =
149
- markSideEffectFreeOpt &&
150
- !entryPaths.has(cleanId) &&
151
- isSideEffectFree(info?.sideEffects);
150
+ markSideEffectFreeOpt && !isEntry && sideEffectFreePkg;
152
151
 
153
152
  const anchors = collectPureAnchorsCached(code, cleanId);
154
- if (anchors === null) {
153
+ // Annotating a call whose result is BOUND (variable initializer,
154
+ // export) only lets the minifier drop it when the binding itself
155
+ // is unused — safe everywhere. Annotating a call whose result is
156
+ // DISCARDED (a bare expression statement like `app.get("/", h)`)
157
+ // is an instruction to delete the call under minification, so it
158
+ // is only applied when the owning package explicitly declared
159
+ // itself side-effect free, and never to entry modules, whose side
160
+ // effects we always preserve (#949).
161
+ const annotateDiscarded = !isEntry && sideEffectFreePkg;
162
+ const positions =
163
+ anchors === null
164
+ ? []
165
+ : annotateDiscarded
166
+ ? [...anchors.bound, ...anchors.discarded]
167
+ : anchors.bound;
168
+ if (positions.length === 0) {
155
169
  // Metadata-only result: omitting `code` tells rolldown the
156
170
  // source was NOT transformed, so no sourcemap is expected and
157
171
  // no SOURCEMAP_BROKEN warning is emitted.
@@ -162,7 +176,7 @@ export const purePlugin = (
162
176
  // rolldown's native side (computed on a background thread). The
163
177
  // fallback covers direct hook invocations (unit tests).
164
178
  const s = meta.magicString ?? new RolldownMagicString(code);
165
- for (const anchor of anchors) s.appendLeft(anchor, PURE_COMMENT);
179
+ for (const anchor of positions) s.appendLeft(anchor, PURE_COMMENT);
166
180
  return {
167
181
  code: s,
168
182
  moduleSideEffects: markSideEffectFree ? false : null,
@@ -324,14 +338,14 @@ export function packageNameFromId(id: string): string | null {
324
338
  */
325
339
  const anchorsCache = new Map<
326
340
  string,
327
- { readonly code: string; readonly anchors: number[] | null }
341
+ { readonly code: string; readonly anchors: PureAnchors | null }
328
342
  >();
329
343
  const ANCHORS_CACHE_MAX = 10_000;
330
344
 
331
345
  const collectPureAnchorsCached = (
332
346
  code: string,
333
347
  filename: string,
334
- ): number[] | null => {
348
+ ): PureAnchors | null => {
335
349
  const cached = anchorsCache.get(filename);
336
350
  if (cached !== undefined && cached.code === code) {
337
351
  return cached.anchors;
@@ -344,16 +358,38 @@ const collectPureAnchorsCached = (
344
358
  return anchors;
345
359
  };
346
360
 
361
+ /**
362
+ * Offsets at which `/*#__PURE__*\/` may be inserted, split by whether the
363
+ * call's result is consumed.
364
+ */
365
+ export interface PureAnchors {
366
+ /**
367
+ * Calls whose result is bound — variable initializers, exports,
368
+ * assignment right-hand sides. Annotating these only lets the minifier
369
+ * drop the call when the binding itself is unused, so it is safe for
370
+ * any module.
371
+ */
372
+ readonly bound: number[];
373
+ /**
374
+ * Top-level calls whose result is discarded (bare expression
375
+ * statements such as `app.get("/", handler)`). Annotating these tells
376
+ * the minifier the whole statement is deletable, so they are only safe
377
+ * in packages that explicitly declare `sideEffects: false` / `[]`.
378
+ */
379
+ readonly discarded: number[];
380
+ }
381
+
347
382
  /**
348
383
  * Parses `code` and returns the offsets at which `/*#__PURE__*\/` must be
349
384
  * inserted — before every top-level `CallExpression` callee / `new`
350
- * keyword. Returns `null` if the file does not need to be modified
351
- * (parse failure or no annotations needed).
385
+ * keyword — classified by whether the call result is bound or discarded.
386
+ * Returns `null` if the file does not need to be modified (parse failure
387
+ * or no annotations needed).
352
388
  */
353
389
  export function collectPureAnchors(
354
390
  code: string,
355
391
  filename: string,
356
- ): number[] | null {
392
+ ): PureAnchors | null {
357
393
  let program: Program;
358
394
  try {
359
395
  // Use TS lang so the parser tolerates TS syntax (`as`, `satisfies`,
@@ -364,51 +400,66 @@ export function collectPureAnchors(
364
400
  return null;
365
401
  }
366
402
 
367
- const anchors: number[] = [];
403
+ const bound: number[] = [];
404
+ const discarded: number[] = [];
368
405
 
369
- const visitCall = (call: CallExpression | NewExpression) => {
406
+ const visitCall = (
407
+ call: CallExpression | NewExpression,
408
+ isDiscarded: boolean,
409
+ ) => {
370
410
  if (isIIFE(call)) return;
371
411
  // For `new X()`, anchor BEFORE the `new` keyword so we get
372
412
  // `/*#__PURE__*/ new X()` (matches babel-plugin-annotate-pure-calls).
373
413
  const anchor =
374
414
  call.type === "NewExpression" ? call.start : call.callee.start;
375
415
  if (alreadyAnnotated(code, anchor)) return;
376
- anchors.push(anchor);
416
+ (isDiscarded ? discarded : bound).push(anchor);
377
417
  };
378
418
 
379
- const visitExpression = (expr: Expression | null | undefined) => {
419
+ const visitExpression = (
420
+ expr: Expression | null | undefined,
421
+ isDiscarded: boolean,
422
+ ) => {
380
423
  if (!expr) return;
381
424
  switch (expr.type) {
382
425
  case "CallExpression":
383
426
  case "NewExpression":
384
- visitCall(expr);
427
+ visitCall(expr, isDiscarded);
385
428
  return;
386
- case "SequenceExpression":
387
- for (const inner of expr.expressions) visitExpression(inner);
429
+ case "SequenceExpression": {
430
+ // All but the last expression's results are always discarded;
431
+ // the last one takes the surrounding context.
432
+ const exprs = expr.expressions;
433
+ for (let i = 0; i < exprs.length; i++) {
434
+ visitExpression(exprs[i], i < exprs.length - 1 ? true : isDiscarded);
435
+ }
388
436
  return;
437
+ }
389
438
  case "ParenthesizedExpression":
390
- visitExpression(expr.expression);
439
+ visitExpression(expr.expression, isDiscarded);
391
440
  return;
392
441
  case "LogicalExpression":
393
- visitExpression(expr.left);
394
- visitExpression(expr.right);
442
+ visitExpression(expr.left, isDiscarded);
443
+ visitExpression(expr.right, isDiscarded);
395
444
  return;
396
445
  case "ConditionalExpression":
397
- visitExpression(expr.consequent);
398
- visitExpression(expr.alternate);
446
+ visitExpression(expr.consequent, isDiscarded);
447
+ visitExpression(expr.alternate, isDiscarded);
399
448
  return;
400
449
  case "AssignmentExpression":
401
- visitExpression(expr.right);
450
+ // The right-hand side's result is stored in the target, so it is
451
+ // bound even when the assignment appears in statement position.
452
+ visitExpression(expr.right, false);
402
453
  return;
403
454
  case "TSAsExpression":
404
455
  case "TSSatisfiesExpression":
405
456
  case "TSNonNullExpression":
406
457
  case "TSTypeAssertion":
407
- visitExpression(expr.expression);
458
+ visitExpression(expr.expression, isDiscarded);
408
459
  return;
409
460
  case "ChainExpression": {
410
461
  const inner = expr.expression;
411
- if (inner.type === "CallExpression") visitCall(inner);
462
+ if (inner.type === "CallExpression") visitCall(inner, isDiscarded);
412
463
  return;
413
464
  }
414
465
  default:
@@ -419,11 +470,11 @@ export function collectPureAnchors(
419
470
  const visitTopLevel = (node: Statement) => {
420
471
  switch (node.type) {
421
472
  case "ExpressionStatement":
422
- visitExpression((node as ExpressionStatement).expression);
473
+ visitExpression((node as ExpressionStatement).expression, true);
423
474
  return;
424
475
  case "VariableDeclaration":
425
476
  for (const decl of (node as VariableDeclaration).declarations) {
426
- visitExpression(decl.init);
477
+ visitExpression(decl.init, false);
427
478
  }
428
479
  return;
429
480
  case "ExportNamedDeclaration": {
@@ -439,7 +490,7 @@ export function collectPureAnchors(
439
490
  decl.type !== "ClassDeclaration" &&
440
491
  decl.type !== "TSInterfaceDeclaration"
441
492
  ) {
442
- visitExpression(decl as Expression);
493
+ visitExpression(decl as Expression, false);
443
494
  }
444
495
  return;
445
496
  }
@@ -455,7 +506,9 @@ export function collectPureAnchors(
455
506
  visitTopLevel(node as Statement);
456
507
  }
457
508
 
458
- return anchors.length === 0 ? null : anchors;
509
+ return bound.length === 0 && discarded.length === 0
510
+ ? null
511
+ : { bound, discarded };
459
512
  }
460
513
 
461
514
  function isIIFE(node: CallExpression | NewExpression): boolean {