ignotum 0.0.14 → 0.0.15

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 +6039 -4690
  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 +29 -4
  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 +84 -101
  77. package/src/cli/deploy.ts +116 -183
  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,45 +159,6 @@ 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* () {
@@ -297,82 +261,101 @@ export const controlClientLayer = Layer.effect(
297
261
  );
298
262
  });
299
263
 
300
- const beginDeployment = Effect.fn("ControlClient.beginDeployment")(function* (
264
+ const beginVersion = Effect.fn("ControlClient.beginVersion")(function* (
301
265
  appId: AppId,
302
- inventory: DeploymentInventory,
266
+ versionId: VersionId,
267
+ declaration: UploadDeclaration,
303
268
  ) {
304
269
  const request = yield* HttpClientRequest.post(
305
- endpoint(config.apiUrl, controlDeploymentsPath(appId)),
270
+ endpoint(config.apiUrl, `${controlVersionPath(appId, versionId)}/begin`),
306
271
  ).pipe(
307
- HttpClientRequest.schemaBodyJson(BeginDeploymentRequest)({ inventory }),
308
- Effect.mapError((cause) =>
309
- unavailable("The deployment request could not be encoded.", cause),
310
- ),
272
+ HttpClientRequest.schemaBodyJson(UploadDeclaration)(declaration),
273
+ Effect.mapError((cause) => unavailable("The version request could not be encoded.", cause)),
311
274
  );
312
- return yield* execute(request, ControlDeploymentSchema);
275
+ return yield* executeIdempotent(request, BeginVersionResponseSchema);
313
276
  });
314
277
 
315
- const uploadDeploymentFile = Effect.fn("ControlClient.uploadDeploymentFile")(function* (
278
+ const uploadVersionFile = Effect.fn("ControlClient.uploadVersionFile")(function* (
316
279
  appId: AppId,
317
- deploymentId: DeploymentId,
318
- file: DeploymentUploadFile,
280
+ versionId: VersionId,
281
+ file: VersionUploadFile,
319
282
  ) {
320
283
  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
- );
284
+ endpoint(config.apiUrl, controlVersionFilePath(appId, versionId, file.path)),
285
+ ).pipe(HttpClientRequest.setHeader("content-length", String(file.size)));
286
+ const uploadRequest =
287
+ file.contentEncoding === undefined
288
+ ? baseRequest
289
+ : baseRequest.pipe(HttpClientRequest.setHeader("content-encoding", file.contentEncoding));
326
290
  const request = yield* file.bytes === undefined
327
291
  ? file.sourcePath === undefined
328
- ? Effect.fail(unavailable(`Deployment file '${file.path}' has no upload source.`, {}))
329
- : HttpClientRequest.bodyFile(baseRequest, file.sourcePath, {
292
+ ? Effect.fail(unavailable(`Version file '${file.path}' has no upload source.`, {}))
293
+ : HttpClientRequest.bodyFile(uploadRequest, file.sourcePath, {
330
294
  contentType: file.contentType,
331
295
  }).pipe(
332
296
  Effect.provideService(FileSystem.FileSystem, fileSystem),
333
297
  Effect.mapError((cause) =>
334
- unavailable(`Deployment file '${file.path}' could not be opened.`, cause),
298
+ unavailable(`Version file '${file.path}' could not be opened.`, cause),
335
299
  ),
336
300
  )
337
301
  : Effect.succeed(
338
- HttpClientRequest.bodyUint8Array(baseRequest, file.bytes, file.contentType),
302
+ HttpClientRequest.bodyUint8Array(uploadRequest, file.bytes, file.contentType),
339
303
  );
340
- const response = yield* executeIdempotent(request, DeploymentFileUploadResponse);
341
- return response.status;
304
+ const response = yield* idempotentHttp.execute(authenticate(request)).pipe(
305
+ Effect.mapError((cause) => unavailable("The artifact upload failed.", cause)),
306
+ Effect.timeoutOrElse({
307
+ duration: "30 seconds",
308
+ orElse: () => Effect.fail(unavailable("The artifact upload exceeded its deadline.", {})),
309
+ }),
310
+ );
311
+ if (response.status !== 204) {
312
+ yield* decodeResponse(response, Schema.Never);
313
+ }
342
314
  });
343
315
 
344
- const finalizeDeployment = Effect.fn("ControlClient.finalizeDeployment")(function* (
316
+ const finalizeVersion = Effect.fn("ControlClient.finalizeVersion")(function* (
345
317
  appId: AppId,
346
- deploymentId: DeploymentId,
318
+ versionId: VersionId,
347
319
  ) {
348
320
  return yield* executeIdempotent(
349
321
  HttpClientRequest.post(
350
- endpoint(config.apiUrl, controlDeploymentFinalizePath(appId, deploymentId)),
322
+ endpoint(config.apiUrl, controlVersionFinalizePath(appId, versionId)),
351
323
  ),
352
- ControlDeploymentSchema,
324
+ ControlVersionSchema,
353
325
  );
354
326
  });
355
327
 
356
- const activateDeployment = Effect.fn("ControlClient.activateDeployment")(function* (
328
+ const activateVersion = Effect.fn("ControlClient.activateVersion")(function* (
357
329
  appId: AppId,
358
- deploymentId: DeploymentId,
330
+ versionId: VersionId,
359
331
  ) {
360
332
  return yield* executeIdempotent(
361
333
  HttpClientRequest.post(
362
- endpoint(config.apiUrl, controlDeploymentActivatePath(appId, deploymentId)),
334
+ endpoint(config.apiUrl, controlVersionActivatePath(appId, versionId)),
363
335
  ),
364
- ControlAppSchema,
336
+ ExecutionSelection,
337
+ );
338
+ });
339
+
340
+ const getVersion = Effect.fn("ControlClient.getVersion")(function* (
341
+ appId: AppId,
342
+ versionId: VersionId,
343
+ ) {
344
+ return yield* executeIdempotent(
345
+ HttpClientRequest.get(endpoint(config.apiUrl, controlVersionPath(appId, versionId))),
346
+ ControlVersionSchema,
365
347
  );
366
348
  });
367
349
 
368
350
  return ControlClient.of({
369
- activateDeployment,
370
- beginDeployment,
351
+ activateVersion,
352
+ beginVersion,
371
353
  createApp,
372
- finalizeDeployment,
354
+ finalizeVersion,
373
355
  getApp,
356
+ getVersion,
374
357
  getAppBySlug,
375
- uploadDeploymentFile,
358
+ uploadVersionFile,
376
359
  });
377
360
  }),
378
361
  );
package/src/cli/deploy.ts CHANGED
@@ -1,245 +1,178 @@
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),
102
+ (file) => control.uploadVersionFile(configuration.appId, versionId, file),
189
103
  { concurrency: 4, discard: true },
190
104
  ).pipe(
191
105
  Effect.catchTags({
192
106
  ControlClientUnavailable: (cause) =>
193
- DeploymentUploadInterrupted.make({
107
+ VersionUploadInterrupted.make({
194
108
  cause,
195
- deploymentId: deployment.id,
196
- message: `Deployment ${deployment.id} was interrupted during upload. The active deployment was not changed.`,
109
+ versionId,
110
+ message: `Version ${versionId} was interrupted during upload. The active version was not changed.`,
197
111
  }),
198
112
  }),
199
113
  );
200
-
201
- yield* control.finalizeDeployment(configuration.appId, deployment.id).pipe(
114
+ const version = yield* control.finalizeVersion(configuration.appId, versionId).pipe(
202
115
  Effect.catchTags({
203
116
  ControlClientUnavailable: (cause) =>
204
- DeploymentUploadInterrupted.make({
117
+ VersionUploadInterrupted.make({
205
118
  cause,
206
- deploymentId: deployment.id,
207
- message: `Deployment ${deployment.id} could not be finalized. The active deployment was not changed.`,
119
+ versionId,
120
+ message: `Version ${versionId} could not be finalized. The active version was not changed.`,
208
121
  }),
209
122
  }),
123
+ Effect.flatMap(readyVersion),
210
124
  );
211
-
212
- yield* control
213
- .activateDeployment(configuration.appId, deployment.id)
214
- .pipe(Effect.mapError((cause) => activationError(deployment.id, cause)));
215
-
216
- return deployment.id;
125
+ const selection = yield* control
126
+ .activateVersion(configuration.appId, versionId)
127
+ .pipe(Effect.mapError((cause) => activationError(version, cause)));
128
+ if (selection.appId !== configuration.appId || selection.versionId !== versionId) {
129
+ return yield* activationError(
130
+ version,
131
+ ControlClientUnavailable.make({
132
+ cause: selection,
133
+ message: "The API confirmed a different App Version.",
134
+ }),
135
+ );
136
+ }
137
+ return { versionId, versionNumber: version.number, selection };
217
138
  });
218
139
 
219
140
  export const deploy = Effect.fn("Deploy.run")(function* (
220
141
  appDirectory: string,
221
142
  requestedSlug: AppSlug | undefined,
222
143
  ) {
223
- const build = yield* buildDeploymentArtifact(appDirectory);
144
+ const build = yield* buildVersionArtifact(appDirectory);
224
145
  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;
146
+ const activated = yield* uploadVersion(configuration, build);
147
+ return { ...activated, appUrl: appUrl(configuration.slug) };
232
148
  });
233
149
 
234
- export const resumeDeployment = Effect.fn("Deploy.resume")(function* (
150
+ export const resumeVersion = Effect.fn("Deploy.resume")(function* (
235
151
  appDirectory: string,
236
- deploymentId: DeploymentId,
152
+ versionId: VersionId,
237
153
  requestedSlug?: AppSlug,
238
154
  ) {
239
155
  const configuration = yield* configureApp(appDirectory, requestedSlug);
240
156
  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 };
157
+ const version = yield* control
158
+ .getVersion(configuration.appId, versionId)
159
+ .pipe(Effect.flatMap(readyVersion));
160
+ const selection = yield* control
161
+ .activateVersion(configuration.appId, versionId)
162
+ .pipe(Effect.mapError((cause) => activationError(version, cause)));
163
+ if (selection.appId !== configuration.appId || selection.versionId !== versionId) {
164
+ return yield* activationError(
165
+ version,
166
+ ControlClientUnavailable.make({
167
+ cause: selection,
168
+ message: "The API confirmed a different App Version.",
169
+ }),
170
+ );
171
+ }
172
+ return {
173
+ appUrl: appUrl(configuration.slug),
174
+ versionId,
175
+ versionNumber: version.number,
176
+ selection,
177
+ };
245
178
  });