ignotum 0.0.13 → 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 (131) hide show
  1. package/README.md +6 -6
  2. package/dist/cli/bin.mjs +6111 -4448
  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 -1138
  21. package/dist/runtime/client.js.map +1 -1
  22. package/dist/runtime/descriptor-CTY4mtwS-cS43EbwS.js +269 -0
  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-D4seNHlX.d.ts +184 -0
  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 +25 -8
  41. package/dist/runtime/internal/host.js +27 -6
  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 -1
  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-1Zs03-iS.js → schema-D1wOqiNH.js} +40 -12
  58. package/dist/runtime/schema-D1wOqiNH.js.map +1 -0
  59. package/dist/runtime/server.d.ts +5 -5
  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-DeCCyqLU-BIm7rU-0.d.ts +341 -0
  65. package/package.json +29 -4
  66. package/src/cli/agent-files.ts +14 -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 +51 -220
  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 +28 -5
  76. package/src/cli/control-client.ts +84 -101
  77. package/src/cli/deploy.ts +133 -177
  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 +66 -592
  104. package/src/dev-runtime/database.ts +77 -2
  105. package/src/dev-runtime/functions.ts +3 -3
  106. package/src/dev-runtime/id.ts +19 -4
  107. package/src/dev-runtime/managed-functions.ts +255 -0
  108. package/src/dev-runtime/managed-sync.ts +263 -0
  109. package/src/dev-runtime/managed-websocket.ts +71 -0
  110. package/src/dev-runtime/migrations.ts +20 -0
  111. package/src/dev-runtime/sync.ts +236 -236
  112. package/src/image-assets.d.ts +36 -0
  113. package/src/internal/client.ts +186 -0
  114. package/src/internal/host.ts +1 -1
  115. package/src/internal/http-paths.ts +0 -3
  116. package/src/internal/routes.ts +45 -0
  117. package/dist/runtime/api-CAgKDij7.js.map +0 -1
  118. package/dist/runtime/descriptor-Cyo9FP9n-C0SRVXNW.js +0 -154
  119. package/dist/runtime/descriptor-Cyo9FP9n-C0SRVXNW.js.map +0 -1
  120. package/dist/runtime/id-Bt9XWRGL.js.map +0 -1
  121. package/dist/runtime/id-Dz0apuB3.d.ts +0 -1
  122. package/dist/runtime/index-D54flWtH.d.ts +0 -402
  123. package/dist/runtime/pagination-DnKg3dkI-r5ZUxBBx.d.ts +0 -112
  124. package/dist/runtime/pagination-Dz0apuB3.d.ts +0 -1
  125. package/dist/runtime/result-DKAA4gpS.d.ts +0 -1
  126. package/dist/runtime/schema-1Zs03-iS.js.map +0 -1
  127. package/src/cli/build/client.ts +0 -119
  128. package/src/cli/build/public.ts +0 -186
  129. package/src/cli/client-entry.ts +0 -152
  130. package/src/cli/client-plugin.ts +0 -136
  131. 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,222 +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";
18
- import { ControlClient, deploymentUploadFiles } from "./control-client.js";
19
-
20
- export interface DeploymentBuildResult {
21
- readonly clientAssets: number;
22
- readonly inventory: DeploymentInventory;
23
- readonly inventoryBytes: Uint8Array;
24
- readonly outputDirectory: string;
25
- readonly memoryFiles?: ReadonlyMap<string, Uint8Array>;
26
- readonly serverFunctions: number;
27
- }
28
-
29
- export interface DeployResult extends DeploymentBuildResult {
30
- readonly appUrl: string;
31
- readonly deploymentId: DeploymentId;
32
- }
33
-
34
- export class BuildReplacementFailed extends Schema.TaggedError<BuildReplacementFailed>()(
35
- "BuildReplacementFailed",
36
- {
37
- backupPath: Schema.String,
38
- cause: Schema.Defect(),
39
- message: Schema.String,
40
- },
41
- ) {}
42
-
43
- export class DeploymentUploadInterrupted extends Schema.TaggedError<DeploymentUploadInterrupted>()(
44
- "DeploymentUploadInterrupted",
45
- {
46
- cause: Schema.Defect(),
47
- deploymentId: DeploymentId,
48
- message: Schema.String,
49
- },
6
+ import { buildManagedVersion, type ManagedVersionBuildResult } from "./build/managed.js";
7
+ import {
8
+ ControlClient,
9
+ ControlClientUnavailable,
10
+ type ControlApiError,
11
+ type VersionUploadFile,
12
+ } from "./control-client.js";
13
+
14
+ export class VersionUploadInterrupted extends Schema.TaggedError<VersionUploadInterrupted>()(
15
+ "VersionUploadInterrupted",
16
+ { cause: Schema.Defect(), versionId: VersionId, message: Schema.String },
50
17
  ) {}
51
18
 
52
- export class DeploymentActivationUnconfirmed extends Schema.TaggedError<DeploymentActivationUnconfirmed>()(
53
- "DeploymentActivationUnconfirmed",
19
+ export class VersionActivationUnconfirmed extends Schema.TaggedError<VersionActivationUnconfirmed>()(
20
+ "VersionActivationUnconfirmed",
54
21
  {
55
22
  cause: Schema.Defect(),
56
- deploymentId: DeploymentId,
23
+ versionId: VersionId,
24
+ versionNumber: VersionNumber,
57
25
  message: Schema.String,
58
26
  },
59
27
  ) {}
60
28
 
61
- const replaceBuildDirectory = Effect.fn("Deploy.replaceBuildDirectory")(function* (
62
- stateDirectory: string,
63
- stagingDirectory: string,
64
- buildDirectory: string,
65
- ) {
66
- const fileSystem = yield* FileSystem.FileSystem;
67
- const currentExists = yield* fileSystem.exists(buildDirectory);
68
- if (!currentExists) {
69
- yield* fileSystem.rename(stagingDirectory, buildDirectory);
70
- return;
71
- }
72
-
73
- const backupPath = yield* fileSystem.makeTempDirectory({
74
- directory: stateDirectory,
75
- prefix: ".build-backup-",
76
- });
77
- yield* fileSystem.remove(backupPath, { recursive: true });
78
- yield* fileSystem.rename(buildDirectory, backupPath);
79
-
80
- const replacement = yield* Effect.result(fileSystem.rename(stagingDirectory, buildDirectory));
81
- if (Result.isSuccess(replacement)) {
82
- yield* fileSystem.remove(backupPath, { recursive: true });
83
- return;
84
- }
85
-
86
- const restoration = yield* Effect.result(fileSystem.rename(backupPath, buildDirectory));
87
- if (Result.isFailure(restoration)) {
88
- return yield* BuildReplacementFailed.make({
89
- backupPath,
90
- cause: restoration.failure,
91
- message: `Could not install the new build or restore the previous build. The previous build remains at ${backupPath}.`,
92
- });
93
- }
94
-
95
- return yield* replacement.failure;
96
- });
29
+ const activationError = (
30
+ version: ControlVersion & { readonly number: VersionNumber },
31
+ cause: ControlApiError | ControlClientUnavailable,
32
+ ) =>
33
+ cause._tag === "ControlClientUnavailable" || cause.status >= 500
34
+ ? VersionActivationUnconfirmed.make({
35
+ cause,
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.`,
39
+ })
40
+ : cause;
41
+
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
+ );
97
51
 
98
- export const buildDeploymentArtifact = Effect.fn("Deploy.buildArtifact")(function* (
52
+ export const buildVersionArtifact = Effect.fn("Deploy.buildArtifact")(function* (
99
53
  appDirectory: string,
100
54
  ) {
101
- const fileSystem = yield* FileSystem.FileSystem;
102
55
  const path = yield* Path.Path;
103
- const stateDirectory = path.join(appDirectory, ".ignotum");
104
- const buildDirectory = path.join(stateDirectory, "build");
105
-
106
- yield* fileSystem.makeDirectory(stateDirectory, { recursive: true });
107
- const codegen = yield* generate(appDirectory);
108
-
109
- return yield* Effect.scoped(
110
- Effect.gen(function* () {
111
- const stagingDirectory = yield* Effect.acquireRelease(
112
- fileSystem.makeTempDirectory({
113
- directory: stateDirectory,
114
- prefix: ".build-staging-",
115
- }),
116
- (directory) =>
117
- fileSystem.remove(directory, { force: true, recursive: true }).pipe(Effect.orDie),
118
- );
119
- const clientOutput = path.join(stagingDirectory, "client");
120
- const serverOutput = path.join(stagingDirectory, "server");
121
- const client = yield* buildClient(appDirectory, clientOutput);
122
- const server = yield* buildServer(appDirectory, serverOutput, codegen.functionModules);
123
- const diskPayloadFiles = yield* readArtifactDirectory(stagingDirectory);
124
- const payloadFiles = [...diskPayloadFiles, server.environment];
125
- const inventory = yield* makeDeploymentInventory(payloadFiles);
126
- yield* fileSystem.writeFile(
127
- path.join(stagingDirectory, deploymentInventoryPath),
128
- inventory.bytes,
129
- );
130
- const diskArtifactFiles = yield* readArtifactDirectory(stagingDirectory);
131
- const artifactFiles = [...diskArtifactFiles, server.environment];
132
- yield* validateDeploymentArtifact(artifactFiles);
133
- yield* Effect.logInfo("Built deployment artifact").pipe(
134
- Effect.annotateLogs({
135
- files: inventory.inventory.files.length,
136
- functions: server.functions,
137
- }),
138
- );
139
-
140
- yield* replaceBuildDirectory(stateDirectory, stagingDirectory, buildDirectory);
141
-
142
- return {
143
- clientAssets: client.assets,
144
- inventory: inventory.inventory,
145
- inventoryBytes: inventory.bytes,
146
- memoryFiles: new Map([[server.environment.path, server.environment.bytes]]),
147
- outputDirectory: buildDirectory,
148
- serverFunctions: server.functions,
149
- } satisfies DeploymentBuildResult;
150
- }),
151
- );
56
+ return yield* buildManagedVersion(appDirectory, { applicationName: path.basename(appDirectory) });
152
57
  });
153
58
 
154
- export const uploadDeployment = Effect.fn("Deploy.upload")(function* (
59
+ export const uploadVersion = Effect.fn("Deploy.upload")(function* (
155
60
  configuration: AppConfiguration,
156
- build: DeploymentBuildResult,
61
+ build: ManagedVersionBuildResult,
157
62
  ) {
158
63
  const control = yield* ControlClient;
159
- const inventorySha256 = yield* sha256(build.inventoryBytes);
160
- const files = yield* deploymentUploadFiles(
161
- build.inventory,
162
- inventorySha256,
163
- build.inventoryBytes.byteLength,
164
- build.outputDirectory,
165
- build.memoryFiles,
166
- );
167
- const deployment = yield* control.beginDeployment(configuration.appId, build.inventory);
168
-
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
+ });
169
100
  yield* Effect.forEach(
170
101
  files,
171
- (file) => control.uploadDeploymentFile(configuration.appId, deployment.id, file),
102
+ (file) => control.uploadVersionFile(configuration.appId, versionId, file),
172
103
  { concurrency: 4, discard: true },
173
104
  ).pipe(
174
105
  Effect.catchTags({
175
106
  ControlClientUnavailable: (cause) =>
176
- DeploymentUploadInterrupted.make({
177
- cause,
178
- deploymentId: deployment.id,
179
- message: `Deployment ${deployment.id} was interrupted during upload. The active deployment was not changed.`,
180
- }),
181
- }),
182
- );
183
-
184
- yield* control.finalizeDeployment(configuration.appId, deployment.id).pipe(
185
- Effect.catchTags({
186
- ControlClientUnavailable: (cause) =>
187
- DeploymentUploadInterrupted.make({
107
+ VersionUploadInterrupted.make({
188
108
  cause,
189
- deploymentId: deployment.id,
190
- message: `Deployment ${deployment.id} could not be finalized. The active deployment was not changed.`,
109
+ versionId,
110
+ message: `Version ${versionId} was interrupted during upload. The active version was not changed.`,
191
111
  }),
192
112
  }),
193
113
  );
194
-
195
- yield* control.activateDeployment(configuration.appId, deployment.id).pipe(
114
+ const version = yield* control.finalizeVersion(configuration.appId, versionId).pipe(
196
115
  Effect.catchTags({
197
116
  ControlClientUnavailable: (cause) =>
198
- DeploymentActivationUnconfirmed.make({
117
+ VersionUploadInterrupted.make({
199
118
  cause,
200
- deploymentId: deployment.id,
201
- message: `Deployment ${deployment.id} is ready, but activation could not be confirmed.`,
119
+ versionId,
120
+ message: `Version ${versionId} could not be finalized. The active version was not changed.`,
202
121
  }),
203
122
  }),
123
+ Effect.flatMap(readyVersion),
204
124
  );
205
-
206
- 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 };
207
138
  });
208
139
 
209
140
  export const deploy = Effect.fn("Deploy.run")(function* (
210
141
  appDirectory: string,
211
142
  requestedSlug: AppSlug | undefined,
212
143
  ) {
213
- const build = yield* buildDeploymentArtifact(appDirectory);
144
+ const build = yield* buildVersionArtifact(appDirectory);
214
145
  const configuration = yield* configureApp(appDirectory, requestedSlug);
215
- const deploymentId = yield* uploadDeployment(configuration, build);
146
+ const activated = yield* uploadVersion(configuration, build);
147
+ return { ...activated, appUrl: appUrl(configuration.slug) };
148
+ });
216
149
 
150
+ export const resumeVersion = Effect.fn("Deploy.resume")(function* (
151
+ appDirectory: string,
152
+ versionId: VersionId,
153
+ requestedSlug?: AppSlug,
154
+ ) {
155
+ const configuration = yield* configureApp(appDirectory, requestedSlug);
156
+ const control = yield* ControlClient;
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
+ }
217
172
  return {
218
- ...build,
219
173
  appUrl: appUrl(configuration.slug),
220
- deploymentId,
221
- } satisfies DeployResult;
174
+ versionId,
175
+ versionNumber: version.number,
176
+ selection,
177
+ };
222
178
  });