ignotum 0.0.14 → 0.0.16

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 (127) hide show
  1. package/README.md +6 -6
  2. package/dist/cli/bin.mjs +6042 -4693
  3. package/dist/cli/bin.mjs.map +1 -1
  4. package/dist/cli/emscripten-module-D5GzfUNv.mjs +533 -0
  5. package/dist/cli/emscripten-module-D5GzfUNv.mjs.map +1 -0
  6. package/dist/cli/ffi-7IElU4Co.mjs +148 -0
  7. package/dist/cli/ffi-7IElU4Co.mjs.map +1 -0
  8. package/dist/cli/managed-release-WHYLZwaS.mjs +10902 -0
  9. package/dist/cli/managed-release-WHYLZwaS.mjs.map +1 -0
  10. package/dist/cli/managed-release.d.mts +1 -0
  11. package/dist/cli/managed-release.mjs +14 -0
  12. package/dist/cli/managed-release.mjs.map +1 -0
  13. package/dist/cli/module-ES6BEMUI-D36I4NVn.mjs +1289 -0
  14. package/dist/cli/module-ES6BEMUI-D36I4NVn.mjs.map +1 -0
  15. package/dist/runtime/{api-Cv3hMbzo.d.ts → api-B4fwp5a2.d.ts} +4 -4
  16. package/dist/runtime/bootstrap-BISQSn_O.js +916 -0
  17. package/dist/runtime/bootstrap-BISQSn_O.js.map +1 -0
  18. package/dist/runtime/bootstrap-BTFScKAc.d.ts +545 -0
  19. package/dist/runtime/client.d.ts +50 -15
  20. package/dist/runtime/client.js +7 -1158
  21. package/dist/runtime/client.js.map +1 -1
  22. package/dist/runtime/{descriptor-DCCz5AHd-_nhlnu8U.js → descriptor-CTY4mtwS-cS43EbwS.js} +3 -32
  23. package/dist/runtime/descriptor-CTY4mtwS-cS43EbwS.js.map +1 -0
  24. package/dist/runtime/{id-Bt9XWRGL.js → id--1m0NuUL.js} +4 -4
  25. package/dist/runtime/id--1m0NuUL.js.map +1 -0
  26. package/dist/runtime/{id-BzFHf3Wo-DGPCjgrf.d.ts → id-7an3nxTu-DZJSM5xh.d.ts} +2 -2
  27. package/dist/runtime/id-CBOt2kDo.d.ts +1 -0
  28. package/dist/runtime/identity-QjtJRxBD.d.ts +2 -0
  29. package/dist/runtime/image-Djtjy4Z8.js +19 -0
  30. package/dist/runtime/image-Djtjy4Z8.js.map +1 -0
  31. package/dist/runtime/{index-DWU-7TIh.d.ts → index-D4seNHlX.d.ts} +3 -4
  32. package/dist/runtime/internal/api.d.ts +1 -1
  33. package/dist/runtime/internal/api.js +30 -1
  34. package/dist/runtime/internal/api.js.map +1 -0
  35. package/dist/runtime/internal/bootstrap.d.ts +2 -0
  36. package/dist/runtime/internal/bootstrap.js +2 -0
  37. package/dist/runtime/internal/client.d.ts +2 -0
  38. package/dist/runtime/internal/client.js +75 -0
  39. package/dist/runtime/internal/client.js.map +1 -0
  40. package/dist/runtime/internal/host.d.ts +6 -7
  41. package/dist/runtime/internal/host.js +5 -4
  42. package/dist/runtime/internal/host.js.map +1 -1
  43. package/dist/runtime/internal/routes.d.ts +2 -0
  44. package/dist/runtime/internal/routes.js +2 -0
  45. package/dist/runtime/internal/server.d.ts +1 -1
  46. package/dist/runtime/internal/server.js +1 -1
  47. package/dist/runtime/internal/types.d.ts +2 -2
  48. package/dist/runtime/internal/types.js +1 -1
  49. package/dist/runtime/pagination-CBOt2kDo.d.ts +1 -0
  50. package/dist/runtime/pagination-D3qd6s9N.js +33 -0
  51. package/dist/runtime/pagination-D3qd6s9N.js.map +1 -0
  52. package/dist/runtime/result-BkziOG1L.d.ts +1 -0
  53. package/dist/runtime/router-BSBI1oN1.js +2377 -0
  54. package/dist/runtime/router-BSBI1oN1.js.map +1 -0
  55. package/dist/runtime/routes-Ce8TVP-T.js +690 -0
  56. package/dist/runtime/routes-Ce8TVP-T.js.map +1 -0
  57. package/dist/runtime/{schema-DqogEydG.js → schema-D1wOqiNH.js} +4 -3
  58. package/dist/runtime/schema-D1wOqiNH.js.map +1 -0
  59. package/dist/runtime/server.d.ts +5 -6
  60. package/dist/runtime/server.js +2 -2
  61. package/dist/runtime/{sync-Bs8J3fIr.d.ts → sync-D-GK2sv9.d.ts} +3 -2
  62. package/dist/runtime/{api-CAgKDij7.js → sync-DzUwA8W9.js} +22 -41
  63. package/dist/runtime/sync-DzUwA8W9.js.map +1 -0
  64. package/dist/runtime/{types-C1iDP2Yf-BnkfBOQf.d.ts → types-DeCCyqLU-BIm7rU-0.d.ts} +123 -6
  65. package/package.json +30 -5
  66. package/src/cli/agent-files.ts +10 -8
  67. package/src/cli/build/managed-client.ts +411 -0
  68. package/src/cli/build/managed-server.ts +294 -0
  69. package/src/cli/build/managed.ts +67 -0
  70. package/src/cli/build/output.ts +47 -0
  71. package/src/cli/build/server.ts +49 -247
  72. package/src/cli/client-config.ts +1 -140
  73. package/src/cli/client-styles.ts +1 -0
  74. package/src/cli/codegen.ts +9 -4
  75. package/src/cli/command.ts +9 -9
  76. package/src/cli/control-client.ts +88 -103
  77. package/src/cli/deploy.ts +120 -184
  78. package/src/cli/dev.ts +506 -107
  79. package/src/cli/image-assets.ts +499 -0
  80. package/src/cli/managed-client-boundaries.ts +103 -0
  81. package/src/cli/managed-dev-platform.ts +118 -0
  82. package/src/cli/managed-exports.ts +219 -0
  83. package/src/cli/managed-release-bin.ts +13 -0
  84. package/src/cli/managed-release.ts +260 -0
  85. package/src/cli/module-boundaries.ts +11 -1
  86. package/src/cli/new-app.ts +111 -87
  87. package/src/cli/route-codegen.ts +311 -0
  88. package/src/cli/route-static.ts +710 -0
  89. package/src/client/bootstrap.ts +110 -0
  90. package/src/client/errors.ts +13 -7
  91. package/src/client/files.ts +64 -0
  92. package/src/client/id.ts +9 -5
  93. package/src/client/image.ts +28 -0
  94. package/src/client/index.ts +25 -2
  95. package/src/client/managed-client.ts +729 -0
  96. package/src/client/managed-upgrade.ts +283 -0
  97. package/src/client/recovery-journal.ts +195 -0
  98. package/src/client/route-boundaries.ts +184 -0
  99. package/src/client/router-history.ts +152 -0
  100. package/src/client/router-store.ts +644 -0
  101. package/src/client/router.ts +324 -0
  102. package/src/client/routes.ts +28 -0
  103. package/src/client/sync.ts +65 -591
  104. package/src/dev-runtime/database.ts +2 -2
  105. package/src/dev-runtime/functions.ts +3 -3
  106. package/src/dev-runtime/managed-functions.ts +255 -0
  107. package/src/dev-runtime/managed-sync.ts +263 -0
  108. package/src/dev-runtime/managed-websocket.ts +71 -0
  109. package/src/dev-runtime/migrations.ts +11 -0
  110. package/src/dev-runtime/sync.ts +236 -236
  111. package/src/image-assets.d.ts +36 -0
  112. package/src/internal/client.ts +186 -0
  113. package/src/internal/http-paths.ts +0 -3
  114. package/src/internal/routes.ts +45 -0
  115. package/dist/runtime/api-CAgKDij7.js.map +0 -1
  116. package/dist/runtime/descriptor-DCCz5AHd-_nhlnu8U.js.map +0 -1
  117. package/dist/runtime/id-Bt9XWRGL.js.map +0 -1
  118. package/dist/runtime/id-E7JKqQ9r.d.ts +0 -1
  119. package/dist/runtime/pagination-CpYCaRYD-C2Rpp4Cw.d.ts +0 -122
  120. package/dist/runtime/pagination-E7JKqQ9r.d.ts +0 -1
  121. package/dist/runtime/result-DKAA4gpS.d.ts +0 -1
  122. package/dist/runtime/schema-DqogEydG.js.map +0 -1
  123. package/src/cli/build/client.ts +0 -119
  124. package/src/cli/build/public.ts +0 -186
  125. package/src/cli/client-entry.ts +0 -152
  126. package/src/cli/client-plugin.ts +0 -136
  127. package/src/client/app.ts +0 -15
@@ -4,29 +4,27 @@ import { isIP } from "node:net";
4
4
  import type {
5
5
  AppSlug,
6
6
  ControlApp,
7
- ControlDeployment,
8
- DeploymentFileUploadStatus,
7
+ ControlVersion,
8
+ BeginVersionResponse,
9
9
  } from "@ignotum/contracts/control";
10
10
  import {
11
- BeginDeploymentRequest,
11
+ BeginVersionResponse as BeginVersionResponseSchema,
12
12
  ControlApp as ControlAppSchema,
13
- ControlDeployment as ControlDeploymentSchema,
13
+ ControlVersion as ControlVersionSchema,
14
14
  ControlErrorCode as ControlErrorCodeSchema,
15
15
  ControlErrorResponse,
16
16
  CreateAppRequest,
17
- DeploymentFileUploadResponse,
18
17
  controlAppBySlugPath,
19
18
  controlAppPath,
20
19
  controlAppsPath,
21
- controlDeploymentActivatePath,
22
- controlDeploymentFilePath,
23
- controlDeploymentFinalizePath,
24
- controlDeploymentsPath,
25
- controlUploadHeaderNames,
20
+ controlVersionActivatePath,
21
+ controlVersionFilePath,
22
+ controlVersionFinalizePath,
23
+ controlVersionPath,
26
24
  } from "@ignotum/contracts/control";
27
- import type { ArtifactFile, DeploymentInventory } from "@ignotum/contracts/deployment";
28
- import { deploymentInventoryPath } from "@ignotum/contracts/deployment";
29
- import type { AppId, DeploymentId } from "@ignotum/contracts/runtime/identity";
25
+ import { UploadDeclaration } from "@ignotum/contracts/app-definition";
26
+ import { ExecutionSelection } from "@ignotum/contracts/platform";
27
+ import type { AppId, VersionId } from "@ignotum/contracts/runtime/identity";
30
28
  import {
31
29
  Config,
32
30
  Context,
@@ -34,7 +32,6 @@ import {
34
32
  FileSystem,
35
33
  Layer,
36
34
  Option,
37
- Path,
38
35
  Redacted,
39
36
  Schedule,
40
37
  Schema,
@@ -101,9 +98,10 @@ export class ControlClientUnavailable extends Schema.TaggedError<ControlClientUn
101
98
  },
102
99
  ) {}
103
100
 
104
- export interface DeploymentUploadFile {
101
+ export interface VersionUploadFile {
105
102
  readonly bytes?: Uint8Array;
106
103
  readonly contentType: string;
104
+ readonly contentEncoding?: string;
107
105
  readonly path: string;
108
106
  readonly sha256: string;
109
107
  readonly size: number;
@@ -111,32 +109,37 @@ export interface DeploymentUploadFile {
111
109
  }
112
110
 
113
111
  interface ControlClientService {
114
- readonly activateDeployment: (
112
+ readonly activateVersion: (
115
113
  appId: AppId,
116
- deploymentId: DeploymentId,
117
- ) => Effect.Effect<ControlApp, ControlApiError | ControlClientUnavailable>;
118
- readonly beginDeployment: (
114
+ versionId: VersionId,
115
+ ) => Effect.Effect<ExecutionSelection, ControlApiError | ControlClientUnavailable>;
116
+ readonly beginVersion: (
119
117
  appId: AppId,
120
- inventory: DeploymentInventory,
121
- ) => Effect.Effect<ControlDeployment, ControlApiError | ControlClientUnavailable>;
118
+ versionId: VersionId,
119
+ declaration: UploadDeclaration,
120
+ ) => Effect.Effect<BeginVersionResponse, ControlApiError | ControlClientUnavailable>;
122
121
  readonly createApp: (
123
122
  slug: AppSlug,
124
123
  ) => Effect.Effect<ControlApp, ControlApiError | ControlClientUnavailable>;
125
- readonly finalizeDeployment: (
124
+ readonly finalizeVersion: (
125
+ appId: AppId,
126
+ versionId: VersionId,
127
+ ) => Effect.Effect<ControlVersion, ControlApiError | ControlClientUnavailable>;
128
+ readonly getVersion: (
126
129
  appId: AppId,
127
- deploymentId: DeploymentId,
128
- ) => Effect.Effect<ControlDeployment, ControlApiError | ControlClientUnavailable>;
130
+ versionId: VersionId,
131
+ ) => Effect.Effect<ControlVersion, ControlApiError | ControlClientUnavailable>;
129
132
  readonly getApp: (
130
133
  appId: AppId,
131
134
  ) => Effect.Effect<ControlApp, ControlApiError | ControlClientUnavailable>;
132
135
  readonly getAppBySlug: (
133
136
  slug: AppSlug,
134
137
  ) => Effect.Effect<ControlApp, ControlApiError | ControlClientUnavailable>;
135
- readonly uploadDeploymentFile: (
138
+ readonly uploadVersionFile: (
136
139
  appId: AppId,
137
- deploymentId: DeploymentId,
138
- file: DeploymentUploadFile,
139
- ) => Effect.Effect<DeploymentFileUploadStatus, ControlApiError | ControlClientUnavailable>;
140
+ versionId: VersionId,
141
+ file: VersionUploadFile,
142
+ ) => Effect.Effect<void, ControlApiError | ControlClientUnavailable>;
140
143
  }
141
144
 
142
145
  export class ControlClient extends Context.Service<ControlClient, ControlClientService>()(
@@ -156,55 +159,18 @@ const unavailable = (message: string, cause: unknown) =>
156
159
 
157
160
  const endpoint = (apiUrl: URL, path: string): URL => new URL(path, apiUrl);
158
161
 
159
- const uploadFile = (file: ArtifactFile, sourcePath: string) => ({
160
- contentType: file.contentType,
161
- path: file.path,
162
- sha256: file.sha256,
163
- size: file.size,
164
- sourcePath,
165
- });
166
-
167
- export const deploymentUploadFiles = Effect.fn("ControlClient.deploymentUploadFiles")(function* (
168
- inventory: DeploymentInventory,
169
- inventorySha256: string,
170
- inventorySize: number,
171
- buildDirectory: string,
172
- memoryFiles: ReadonlyMap<string, Uint8Array> = new Map(),
173
- ) {
174
- const path = yield* Path.Path;
175
- return [
176
- {
177
- contentType: "application/json; charset=utf-8",
178
- path: deploymentInventoryPath,
179
- sha256: inventorySha256,
180
- size: inventorySize,
181
- sourcePath: path.join(buildDirectory, deploymentInventoryPath),
182
- },
183
- ...inventory.files.map((file) => {
184
- const bytes = memoryFiles.get(file.path);
185
- return bytes === undefined
186
- ? uploadFile(file, path.join(buildDirectory, file.path))
187
- : {
188
- bytes,
189
- contentType: file.contentType,
190
- path: file.path,
191
- sha256: file.sha256,
192
- size: file.size,
193
- };
194
- }),
195
- ];
196
- });
197
-
198
162
  export const controlClientLayer = Layer.effect(
199
163
  ControlClient,
200
164
  Effect.gen(function* () {
201
165
  const config = yield* HostedControlConfiguration;
202
166
  const fileSystem = yield* FileSystem.FileSystem;
203
167
  const http = yield* HttpClient.HttpClient;
168
+ // Deployment writes are idempotent and may briefly exhaust a constrained
169
+ // control worker while processing the complete module graph.
204
170
  const idempotentHttp = http.pipe(
205
171
  HttpClient.retryTransient({
206
- schedule: Schedule.exponential("100 millis"),
207
- times: 3,
172
+ schedule: Schedule.spaced("100 millis"),
173
+ times: 20,
208
174
  }),
209
175
  );
210
176
 
@@ -297,82 +263,101 @@ export const controlClientLayer = Layer.effect(
297
263
  );
298
264
  });
299
265
 
300
- const beginDeployment = Effect.fn("ControlClient.beginDeployment")(function* (
266
+ const beginVersion = Effect.fn("ControlClient.beginVersion")(function* (
301
267
  appId: AppId,
302
- inventory: DeploymentInventory,
268
+ versionId: VersionId,
269
+ declaration: UploadDeclaration,
303
270
  ) {
304
271
  const request = yield* HttpClientRequest.post(
305
- endpoint(config.apiUrl, controlDeploymentsPath(appId)),
272
+ endpoint(config.apiUrl, `${controlVersionPath(appId, versionId)}/begin`),
306
273
  ).pipe(
307
- HttpClientRequest.schemaBodyJson(BeginDeploymentRequest)({ inventory }),
308
- Effect.mapError((cause) =>
309
- unavailable("The deployment request could not be encoded.", cause),
310
- ),
274
+ HttpClientRequest.schemaBodyJson(UploadDeclaration)(declaration),
275
+ Effect.mapError((cause) => unavailable("The version request could not be encoded.", cause)),
311
276
  );
312
- return yield* execute(request, ControlDeploymentSchema);
277
+ return yield* executeIdempotent(request, BeginVersionResponseSchema);
313
278
  });
314
279
 
315
- const uploadDeploymentFile = Effect.fn("ControlClient.uploadDeploymentFile")(function* (
280
+ const uploadVersionFile = Effect.fn("ControlClient.uploadVersionFile")(function* (
316
281
  appId: AppId,
317
- deploymentId: DeploymentId,
318
- file: DeploymentUploadFile,
282
+ versionId: VersionId,
283
+ file: VersionUploadFile,
319
284
  ) {
320
285
  const baseRequest = HttpClientRequest.put(
321
- endpoint(config.apiUrl, controlDeploymentFilePath(appId, deploymentId, file.path)),
322
- ).pipe(
323
- HttpClientRequest.setHeader(controlUploadHeaderNames.contentLength, String(file.size)),
324
- HttpClientRequest.setHeader(controlUploadHeaderNames.sha256, file.sha256),
325
- );
286
+ endpoint(config.apiUrl, controlVersionFilePath(appId, versionId, file.path)),
287
+ ).pipe(HttpClientRequest.setHeader("content-length", String(file.size)));
288
+ const uploadRequest =
289
+ file.contentEncoding === undefined
290
+ ? baseRequest
291
+ : baseRequest.pipe(HttpClientRequest.setHeader("content-encoding", file.contentEncoding));
326
292
  const request = yield* file.bytes === undefined
327
293
  ? file.sourcePath === undefined
328
- ? Effect.fail(unavailable(`Deployment file '${file.path}' has no upload source.`, {}))
329
- : HttpClientRequest.bodyFile(baseRequest, file.sourcePath, {
294
+ ? Effect.fail(unavailable(`Version file '${file.path}' has no upload source.`, {}))
295
+ : HttpClientRequest.bodyFile(uploadRequest, file.sourcePath, {
330
296
  contentType: file.contentType,
331
297
  }).pipe(
332
298
  Effect.provideService(FileSystem.FileSystem, fileSystem),
333
299
  Effect.mapError((cause) =>
334
- unavailable(`Deployment file '${file.path}' could not be opened.`, cause),
300
+ unavailable(`Version file '${file.path}' could not be opened.`, cause),
335
301
  ),
336
302
  )
337
303
  : Effect.succeed(
338
- HttpClientRequest.bodyUint8Array(baseRequest, file.bytes, file.contentType),
304
+ HttpClientRequest.bodyUint8Array(uploadRequest, file.bytes, file.contentType),
339
305
  );
340
- const response = yield* executeIdempotent(request, DeploymentFileUploadResponse);
341
- return response.status;
306
+ const response = yield* idempotentHttp.execute(authenticate(request)).pipe(
307
+ Effect.mapError((cause) => unavailable("The artifact upload failed.", cause)),
308
+ Effect.timeoutOrElse({
309
+ duration: "30 seconds",
310
+ orElse: () => Effect.fail(unavailable("The artifact upload exceeded its deadline.", {})),
311
+ }),
312
+ );
313
+ if (response.status !== 204) {
314
+ yield* decodeResponse(response, Schema.Never);
315
+ }
342
316
  });
343
317
 
344
- const finalizeDeployment = Effect.fn("ControlClient.finalizeDeployment")(function* (
318
+ const finalizeVersion = Effect.fn("ControlClient.finalizeVersion")(function* (
345
319
  appId: AppId,
346
- deploymentId: DeploymentId,
320
+ versionId: VersionId,
347
321
  ) {
348
322
  return yield* executeIdempotent(
349
323
  HttpClientRequest.post(
350
- endpoint(config.apiUrl, controlDeploymentFinalizePath(appId, deploymentId)),
324
+ endpoint(config.apiUrl, controlVersionFinalizePath(appId, versionId)),
351
325
  ),
352
- ControlDeploymentSchema,
326
+ ControlVersionSchema,
353
327
  );
354
328
  });
355
329
 
356
- const activateDeployment = Effect.fn("ControlClient.activateDeployment")(function* (
330
+ const activateVersion = Effect.fn("ControlClient.activateVersion")(function* (
357
331
  appId: AppId,
358
- deploymentId: DeploymentId,
332
+ versionId: VersionId,
359
333
  ) {
360
334
  return yield* executeIdempotent(
361
335
  HttpClientRequest.post(
362
- endpoint(config.apiUrl, controlDeploymentActivatePath(appId, deploymentId)),
336
+ endpoint(config.apiUrl, controlVersionActivatePath(appId, versionId)),
363
337
  ),
364
- ControlAppSchema,
338
+ ExecutionSelection,
339
+ );
340
+ });
341
+
342
+ const getVersion = Effect.fn("ControlClient.getVersion")(function* (
343
+ appId: AppId,
344
+ versionId: VersionId,
345
+ ) {
346
+ return yield* executeIdempotent(
347
+ HttpClientRequest.get(endpoint(config.apiUrl, controlVersionPath(appId, versionId))),
348
+ ControlVersionSchema,
365
349
  );
366
350
  });
367
351
 
368
352
  return ControlClient.of({
369
- activateDeployment,
370
- beginDeployment,
353
+ activateVersion,
354
+ beginVersion,
371
355
  createApp,
372
- finalizeDeployment,
356
+ finalizeVersion,
373
357
  getApp,
358
+ getVersion,
374
359
  getAppBySlug,
375
- uploadDeploymentFile,
360
+ uploadVersionFile,
376
361
  });
377
362
  }),
378
363
  );
package/src/cli/deploy.ts CHANGED
@@ -1,245 +1,181 @@
1
- import { Effect, FileSystem, Path, Result, Schema } from "effect";
2
-
3
- import { deploymentInventoryPath } from "@ignotum/contracts/deployment";
4
- import type { DeploymentInventory } from "@ignotum/contracts/deployment";
5
- import type { AppSlug } from "@ignotum/contracts/control";
6
- import { DeploymentId } from "@ignotum/contracts/runtime/identity";
7
- import {
8
- makeDeploymentInventory,
9
- readArtifactDirectory,
10
- sha256,
11
- validateDeploymentArtifact,
12
- } from "@ignotum/deployment";
13
-
1
+ import { Effect, Path, Schema } from "effect";
2
+ import type { AppSlug, ControlVersion } from "@ignotum/contracts/control";
3
+ import { VersionId, VersionNumber } from "@ignotum/contracts/runtime/identity";
4
+ import { IdGenerator } from "@ignotum/shared/id";
14
5
  import { appUrl, configureApp, type AppConfiguration } from "./app-configuration.js";
15
- import { buildClient } from "./build/client.js";
16
- import { buildServer } from "./build/server.js";
17
- import { generate } from "./codegen.js";
6
+ import { buildManagedVersion, type ManagedVersionBuildResult } from "./build/managed.js";
18
7
  import {
19
8
  ControlClient,
20
- deploymentUploadFiles,
9
+ ControlClientUnavailable,
21
10
  type ControlApiError,
22
- type ControlClientUnavailable,
11
+ type VersionUploadFile,
23
12
  } from "./control-client.js";
24
13
 
25
- export interface DeploymentBuildResult {
26
- readonly clientAssets: number;
27
- readonly inventory: DeploymentInventory;
28
- readonly inventoryBytes: Uint8Array;
29
- readonly outputDirectory: string;
30
- readonly memoryFiles?: ReadonlyMap<string, Uint8Array>;
31
- readonly serverFunctions: number;
32
- }
33
-
34
- export interface DeployResult extends DeploymentBuildResult {
35
- readonly appUrl: string;
36
- readonly deploymentId: DeploymentId;
37
- }
38
-
39
- export class BuildReplacementFailed extends Schema.TaggedError<BuildReplacementFailed>()(
40
- "BuildReplacementFailed",
41
- {
42
- backupPath: Schema.String,
43
- cause: Schema.Defect(),
44
- message: Schema.String,
45
- },
14
+ export class VersionUploadInterrupted extends Schema.TaggedError<VersionUploadInterrupted>()(
15
+ "VersionUploadInterrupted",
16
+ { cause: Schema.Defect(), versionId: VersionId, message: Schema.String },
46
17
  ) {}
47
18
 
48
- export class DeploymentUploadInterrupted extends Schema.TaggedError<DeploymentUploadInterrupted>()(
49
- "DeploymentUploadInterrupted",
19
+ export class VersionActivationUnconfirmed extends Schema.TaggedError<VersionActivationUnconfirmed>()(
20
+ "VersionActivationUnconfirmed",
50
21
  {
51
22
  cause: Schema.Defect(),
52
- deploymentId: DeploymentId,
53
- message: Schema.String,
54
- },
55
- ) {}
56
-
57
- export class DeploymentActivationUnconfirmed extends Schema.TaggedError<DeploymentActivationUnconfirmed>()(
58
- "DeploymentActivationUnconfirmed",
59
- {
60
- cause: Schema.Defect(),
61
- deploymentId: DeploymentId,
23
+ versionId: VersionId,
24
+ versionNumber: VersionNumber,
62
25
  message: Schema.String,
63
26
  },
64
27
  ) {}
65
28
 
66
29
  const activationError = (
67
- deploymentId: DeploymentId,
30
+ version: ControlVersion & { readonly number: VersionNumber },
68
31
  cause: ControlApiError | ControlClientUnavailable,
69
32
  ) =>
70
33
  cause._tag === "ControlClientUnavailable" || cause.status >= 500
71
- ? DeploymentActivationUnconfirmed.make({
34
+ ? VersionActivationUnconfirmed.make({
72
35
  cause,
73
- deploymentId,
74
- message: `Deployment ${deploymentId} is ready, but activation could not be confirmed. Run ignotum deploy --resume ${deploymentId} to finish activation.`,
36
+ versionId: version.id,
37
+ versionNumber: version.number,
38
+ message: `Version ${version.number} (${version.id}) is ready, but activation could not be confirmed. Run ignotum deploy --resume ${version.id} to finish activation.`,
75
39
  })
76
40
  : cause;
77
41
 
78
- const replaceBuildDirectory = Effect.fn("Deploy.replaceBuildDirectory")(function* (
79
- stateDirectory: string,
80
- stagingDirectory: string,
81
- buildDirectory: string,
82
- ) {
83
- const fileSystem = yield* FileSystem.FileSystem;
84
- const currentExists = yield* fileSystem.exists(buildDirectory);
85
- if (!currentExists) {
86
- yield* fileSystem.rename(stagingDirectory, buildDirectory);
87
- return;
88
- }
89
-
90
- const backupPath = yield* fileSystem.makeTempDirectory({
91
- directory: stateDirectory,
92
- prefix: ".build-backup-",
93
- });
94
- yield* fileSystem.remove(backupPath, { recursive: true });
95
- yield* fileSystem.rename(buildDirectory, backupPath);
96
-
97
- const replacement = yield* Effect.result(fileSystem.rename(stagingDirectory, buildDirectory));
98
- if (Result.isSuccess(replacement)) {
99
- yield* fileSystem.remove(backupPath, { recursive: true });
100
- return;
101
- }
102
-
103
- const restoration = yield* Effect.result(fileSystem.rename(backupPath, buildDirectory));
104
- if (Result.isFailure(restoration)) {
105
- return yield* BuildReplacementFailed.make({
106
- backupPath,
107
- cause: restoration.failure,
108
- message: `Could not install the new build or restore the previous build. The previous build remains at ${backupPath}.`,
109
- });
110
- }
111
-
112
- return yield* replacement.failure;
113
- });
42
+ const readyVersion = (version: ControlVersion) =>
43
+ version.status === "Ready" && version.number !== null
44
+ ? Effect.succeed({ ...version, number: version.number })
45
+ : Effect.fail(
46
+ ControlClientUnavailable.make({
47
+ cause: { versionId: version.id, status: version.status },
48
+ message: `Version ${version.id} is not ready for activation.`,
49
+ }),
50
+ );
114
51
 
115
- export const buildDeploymentArtifact = Effect.fn("Deploy.buildArtifact")(function* (
52
+ export const buildVersionArtifact = Effect.fn("Deploy.buildArtifact")(function* (
116
53
  appDirectory: string,
117
54
  ) {
118
- const fileSystem = yield* FileSystem.FileSystem;
119
55
  const path = yield* Path.Path;
120
- const stateDirectory = path.join(appDirectory, ".ignotum");
121
- const buildDirectory = path.join(stateDirectory, "build");
122
-
123
- yield* fileSystem.makeDirectory(stateDirectory, { recursive: true });
124
- const codegen = yield* generate(appDirectory);
125
-
126
- return yield* Effect.scoped(
127
- Effect.gen(function* () {
128
- const stagingDirectory = yield* Effect.acquireRelease(
129
- fileSystem.makeTempDirectory({
130
- directory: stateDirectory,
131
- prefix: ".build-staging-",
132
- }),
133
- (directory) =>
134
- fileSystem.remove(directory, { force: true, recursive: true }).pipe(Effect.orDie),
135
- );
136
- const clientOutput = path.join(stagingDirectory, "client");
137
- const serverOutput = path.join(stagingDirectory, "server");
138
- const client = yield* buildClient(appDirectory, clientOutput);
139
- const server = yield* buildServer(appDirectory, serverOutput, codegen.functionModules);
140
- const diskPayloadFiles = yield* readArtifactDirectory(stagingDirectory);
141
- const payloadFiles = [...diskPayloadFiles, server.environment];
142
- const inventory = yield* makeDeploymentInventory(payloadFiles);
143
- yield* fileSystem.writeFile(
144
- path.join(stagingDirectory, deploymentInventoryPath),
145
- inventory.bytes,
146
- );
147
- const diskArtifactFiles = yield* readArtifactDirectory(stagingDirectory);
148
- const artifactFiles = [...diskArtifactFiles, server.environment];
149
- yield* validateDeploymentArtifact(artifactFiles);
150
- yield* Effect.logInfo("Built deployment artifact").pipe(
151
- Effect.annotateLogs({
152
- files: inventory.inventory.files.length,
153
- functions: server.functions,
154
- }),
155
- );
156
-
157
- yield* replaceBuildDirectory(stateDirectory, stagingDirectory, buildDirectory);
158
-
159
- return {
160
- clientAssets: client.assets,
161
- inventory: inventory.inventory,
162
- inventoryBytes: inventory.bytes,
163
- memoryFiles: new Map([[server.environment.path, server.environment.bytes]]),
164
- outputDirectory: buildDirectory,
165
- serverFunctions: server.functions,
166
- } satisfies DeploymentBuildResult;
167
- }),
168
- );
56
+ return yield* buildManagedVersion(appDirectory, { applicationName: path.basename(appDirectory) });
169
57
  });
170
58
 
171
- export const uploadDeployment = Effect.fn("Deploy.upload")(function* (
59
+ export const uploadVersion = Effect.fn("Deploy.upload")(function* (
172
60
  configuration: AppConfiguration,
173
- build: DeploymentBuildResult,
61
+ build: ManagedVersionBuildResult,
174
62
  ) {
175
63
  const control = yield* ControlClient;
176
- const inventorySha256 = yield* sha256(build.inventoryBytes);
177
- const files = yield* deploymentUploadFiles(
178
- build.inventory,
179
- inventorySha256,
180
- build.inventoryBytes.byteLength,
181
- build.outputDirectory,
182
- build.memoryFiles,
183
- );
184
- const deployment = yield* control.beginDeployment(configuration.appId, build.inventory);
185
-
64
+ const versionId = yield* (yield* IdGenerator).generate(VersionId);
65
+ const admission = yield* control.beginVersion(configuration.appId, versionId, build.declaration);
66
+ const files = yield* Effect.forEach(admission.targets, (target) => {
67
+ const publicArtifact = build.declaration.definition.artifacts.find(
68
+ (artifact) => artifact.path === target.path,
69
+ );
70
+ const artifact = target.protected ? build.declaration.environment?.artifact : publicArtifact;
71
+ const bytes =
72
+ target.protected && target.path === build.environment.path
73
+ ? build.environment.bytes
74
+ : build.bytes.get(target.path);
75
+ if (
76
+ target.appId !== configuration.appId ||
77
+ target.versionId !== versionId ||
78
+ artifact === undefined ||
79
+ artifact.path !== target.path ||
80
+ artifact.size !== target.size ||
81
+ artifact.sha256 !== target.sha256 ||
82
+ bytes === undefined ||
83
+ bytes.byteLength !== target.size ||
84
+ (target.protected
85
+ ? target.contentType !== "application/json; charset=utf-8" ||
86
+ target.contentEncoding !== undefined
87
+ : publicArtifact === undefined ||
88
+ publicArtifact.contentType !== target.contentType ||
89
+ publicArtifact.contentEncoding !== target.contentEncoding)
90
+ ) {
91
+ return Effect.fail(
92
+ ControlClientUnavailable.make({
93
+ cause: { path: target.path },
94
+ message: "The API requested an artifact that does not match this build.",
95
+ }),
96
+ );
97
+ }
98
+ return Effect.succeed({ ...target, bytes } satisfies VersionUploadFile);
99
+ });
186
100
  yield* Effect.forEach(
187
101
  files,
188
- (file) => control.uploadDeploymentFile(configuration.appId, deployment.id, file),
189
- { concurrency: 4, discard: true },
102
+ (file) => control.uploadVersionFile(configuration.appId, versionId, file),
103
+ // The hosted control plane runs within Cloudflare's per-request CPU budget.
104
+ // Serialize uploads so several validation-heavy requests cannot contend in
105
+ // the same isolate and exhaust that budget together.
106
+ { concurrency: 1, discard: true },
190
107
  ).pipe(
191
108
  Effect.catchTags({
192
109
  ControlClientUnavailable: (cause) =>
193
- DeploymentUploadInterrupted.make({
110
+ VersionUploadInterrupted.make({
194
111
  cause,
195
- deploymentId: deployment.id,
196
- message: `Deployment ${deployment.id} was interrupted during upload. The active deployment was not changed.`,
112
+ versionId,
113
+ message: `Version ${versionId} was interrupted during upload. The active version was not changed.`,
197
114
  }),
198
115
  }),
199
116
  );
200
-
201
- yield* control.finalizeDeployment(configuration.appId, deployment.id).pipe(
117
+ const version = yield* control.finalizeVersion(configuration.appId, versionId).pipe(
202
118
  Effect.catchTags({
203
119
  ControlClientUnavailable: (cause) =>
204
- DeploymentUploadInterrupted.make({
120
+ VersionUploadInterrupted.make({
205
121
  cause,
206
- deploymentId: deployment.id,
207
- message: `Deployment ${deployment.id} could not be finalized. The active deployment was not changed.`,
122
+ versionId,
123
+ message: `Version ${versionId} could not be finalized. The active version was not changed.`,
208
124
  }),
209
125
  }),
126
+ Effect.flatMap(readyVersion),
210
127
  );
211
-
212
- yield* control
213
- .activateDeployment(configuration.appId, deployment.id)
214
- .pipe(Effect.mapError((cause) => activationError(deployment.id, cause)));
215
-
216
- return deployment.id;
128
+ const selection = yield* control
129
+ .activateVersion(configuration.appId, versionId)
130
+ .pipe(Effect.mapError((cause) => activationError(version, cause)));
131
+ if (selection.appId !== configuration.appId || selection.versionId !== versionId) {
132
+ return yield* activationError(
133
+ version,
134
+ ControlClientUnavailable.make({
135
+ cause: selection,
136
+ message: "The API confirmed a different App Version.",
137
+ }),
138
+ );
139
+ }
140
+ return { versionId, versionNumber: version.number, selection };
217
141
  });
218
142
 
219
143
  export const deploy = Effect.fn("Deploy.run")(function* (
220
144
  appDirectory: string,
221
145
  requestedSlug: AppSlug | undefined,
222
146
  ) {
223
- const build = yield* buildDeploymentArtifact(appDirectory);
147
+ const build = yield* buildVersionArtifact(appDirectory);
224
148
  const configuration = yield* configureApp(appDirectory, requestedSlug);
225
- const deploymentId = yield* uploadDeployment(configuration, build);
226
-
227
- return {
228
- ...build,
229
- appUrl: appUrl(configuration.slug),
230
- deploymentId,
231
- } satisfies DeployResult;
149
+ const activated = yield* uploadVersion(configuration, build);
150
+ return { ...activated, appUrl: appUrl(configuration.slug) };
232
151
  });
233
152
 
234
- export const resumeDeployment = Effect.fn("Deploy.resume")(function* (
153
+ export const resumeVersion = Effect.fn("Deploy.resume")(function* (
235
154
  appDirectory: string,
236
- deploymentId: DeploymentId,
155
+ versionId: VersionId,
237
156
  requestedSlug?: AppSlug,
238
157
  ) {
239
158
  const configuration = yield* configureApp(appDirectory, requestedSlug);
240
159
  const control = yield* ControlClient;
241
- yield* control
242
- .activateDeployment(configuration.appId, deploymentId)
243
- .pipe(Effect.mapError((cause) => activationError(deploymentId, cause)));
244
- return { appUrl: appUrl(configuration.slug), deploymentId };
160
+ const version = yield* control
161
+ .getVersion(configuration.appId, versionId)
162
+ .pipe(Effect.flatMap(readyVersion));
163
+ const selection = yield* control
164
+ .activateVersion(configuration.appId, versionId)
165
+ .pipe(Effect.mapError((cause) => activationError(version, cause)));
166
+ if (selection.appId !== configuration.appId || selection.versionId !== versionId) {
167
+ return yield* activationError(
168
+ version,
169
+ ControlClientUnavailable.make({
170
+ cause: selection,
171
+ message: "The API confirmed a different App Version.",
172
+ }),
173
+ );
174
+ }
175
+ return {
176
+ appUrl: appUrl(configuration.slug),
177
+ versionId,
178
+ versionNumber: version.number,
179
+ selection,
180
+ };
245
181
  });