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.
- package/README.md +6 -6
- package/dist/cli/bin.mjs +6042 -4693
- package/dist/cli/bin.mjs.map +1 -1
- package/dist/cli/emscripten-module-D5GzfUNv.mjs +533 -0
- package/dist/cli/emscripten-module-D5GzfUNv.mjs.map +1 -0
- package/dist/cli/ffi-7IElU4Co.mjs +148 -0
- package/dist/cli/ffi-7IElU4Co.mjs.map +1 -0
- package/dist/cli/managed-release-WHYLZwaS.mjs +10902 -0
- package/dist/cli/managed-release-WHYLZwaS.mjs.map +1 -0
- package/dist/cli/managed-release.d.mts +1 -0
- package/dist/cli/managed-release.mjs +14 -0
- package/dist/cli/managed-release.mjs.map +1 -0
- package/dist/cli/module-ES6BEMUI-D36I4NVn.mjs +1289 -0
- package/dist/cli/module-ES6BEMUI-D36I4NVn.mjs.map +1 -0
- package/dist/runtime/{api-Cv3hMbzo.d.ts → api-B4fwp5a2.d.ts} +4 -4
- package/dist/runtime/bootstrap-BISQSn_O.js +916 -0
- package/dist/runtime/bootstrap-BISQSn_O.js.map +1 -0
- package/dist/runtime/bootstrap-BTFScKAc.d.ts +545 -0
- package/dist/runtime/client.d.ts +50 -15
- package/dist/runtime/client.js +7 -1158
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/{descriptor-DCCz5AHd-_nhlnu8U.js → descriptor-CTY4mtwS-cS43EbwS.js} +3 -32
- package/dist/runtime/descriptor-CTY4mtwS-cS43EbwS.js.map +1 -0
- package/dist/runtime/{id-Bt9XWRGL.js → id--1m0NuUL.js} +4 -4
- package/dist/runtime/id--1m0NuUL.js.map +1 -0
- package/dist/runtime/{id-BzFHf3Wo-DGPCjgrf.d.ts → id-7an3nxTu-DZJSM5xh.d.ts} +2 -2
- package/dist/runtime/id-CBOt2kDo.d.ts +1 -0
- package/dist/runtime/identity-QjtJRxBD.d.ts +2 -0
- package/dist/runtime/image-Djtjy4Z8.js +19 -0
- package/dist/runtime/image-Djtjy4Z8.js.map +1 -0
- package/dist/runtime/{index-DWU-7TIh.d.ts → index-D4seNHlX.d.ts} +3 -4
- package/dist/runtime/internal/api.d.ts +1 -1
- package/dist/runtime/internal/api.js +30 -1
- package/dist/runtime/internal/api.js.map +1 -0
- package/dist/runtime/internal/bootstrap.d.ts +2 -0
- package/dist/runtime/internal/bootstrap.js +2 -0
- package/dist/runtime/internal/client.d.ts +2 -0
- package/dist/runtime/internal/client.js +75 -0
- package/dist/runtime/internal/client.js.map +1 -0
- package/dist/runtime/internal/host.d.ts +6 -7
- package/dist/runtime/internal/host.js +5 -4
- package/dist/runtime/internal/host.js.map +1 -1
- package/dist/runtime/internal/routes.d.ts +2 -0
- package/dist/runtime/internal/routes.js +2 -0
- package/dist/runtime/internal/server.d.ts +1 -1
- package/dist/runtime/internal/server.js +1 -1
- package/dist/runtime/internal/types.d.ts +2 -2
- package/dist/runtime/internal/types.js +1 -1
- package/dist/runtime/pagination-CBOt2kDo.d.ts +1 -0
- package/dist/runtime/pagination-D3qd6s9N.js +33 -0
- package/dist/runtime/pagination-D3qd6s9N.js.map +1 -0
- package/dist/runtime/result-BkziOG1L.d.ts +1 -0
- package/dist/runtime/router-BSBI1oN1.js +2377 -0
- package/dist/runtime/router-BSBI1oN1.js.map +1 -0
- package/dist/runtime/routes-Ce8TVP-T.js +690 -0
- package/dist/runtime/routes-Ce8TVP-T.js.map +1 -0
- package/dist/runtime/{schema-DqogEydG.js → schema-D1wOqiNH.js} +4 -3
- package/dist/runtime/schema-D1wOqiNH.js.map +1 -0
- package/dist/runtime/server.d.ts +5 -6
- package/dist/runtime/server.js +2 -2
- package/dist/runtime/{sync-Bs8J3fIr.d.ts → sync-D-GK2sv9.d.ts} +3 -2
- package/dist/runtime/{api-CAgKDij7.js → sync-DzUwA8W9.js} +22 -41
- package/dist/runtime/sync-DzUwA8W9.js.map +1 -0
- package/dist/runtime/{types-C1iDP2Yf-BnkfBOQf.d.ts → types-DeCCyqLU-BIm7rU-0.d.ts} +123 -6
- package/package.json +30 -5
- package/src/cli/agent-files.ts +10 -8
- package/src/cli/build/managed-client.ts +411 -0
- package/src/cli/build/managed-server.ts +294 -0
- package/src/cli/build/managed.ts +67 -0
- package/src/cli/build/output.ts +47 -0
- package/src/cli/build/server.ts +49 -247
- package/src/cli/client-config.ts +1 -140
- package/src/cli/client-styles.ts +1 -0
- package/src/cli/codegen.ts +9 -4
- package/src/cli/command.ts +9 -9
- package/src/cli/control-client.ts +88 -103
- package/src/cli/deploy.ts +120 -184
- package/src/cli/dev.ts +506 -107
- package/src/cli/image-assets.ts +499 -0
- package/src/cli/managed-client-boundaries.ts +103 -0
- package/src/cli/managed-dev-platform.ts +118 -0
- package/src/cli/managed-exports.ts +219 -0
- package/src/cli/managed-release-bin.ts +13 -0
- package/src/cli/managed-release.ts +260 -0
- package/src/cli/module-boundaries.ts +11 -1
- package/src/cli/new-app.ts +111 -87
- package/src/cli/route-codegen.ts +311 -0
- package/src/cli/route-static.ts +710 -0
- package/src/client/bootstrap.ts +110 -0
- package/src/client/errors.ts +13 -7
- package/src/client/files.ts +64 -0
- package/src/client/id.ts +9 -5
- package/src/client/image.ts +28 -0
- package/src/client/index.ts +25 -2
- package/src/client/managed-client.ts +729 -0
- package/src/client/managed-upgrade.ts +283 -0
- package/src/client/recovery-journal.ts +195 -0
- package/src/client/route-boundaries.ts +184 -0
- package/src/client/router-history.ts +152 -0
- package/src/client/router-store.ts +644 -0
- package/src/client/router.ts +324 -0
- package/src/client/routes.ts +28 -0
- package/src/client/sync.ts +65 -591
- package/src/dev-runtime/database.ts +2 -2
- package/src/dev-runtime/functions.ts +3 -3
- package/src/dev-runtime/managed-functions.ts +255 -0
- package/src/dev-runtime/managed-sync.ts +263 -0
- package/src/dev-runtime/managed-websocket.ts +71 -0
- package/src/dev-runtime/migrations.ts +11 -0
- package/src/dev-runtime/sync.ts +236 -236
- package/src/image-assets.d.ts +36 -0
- package/src/internal/client.ts +186 -0
- package/src/internal/http-paths.ts +0 -3
- package/src/internal/routes.ts +45 -0
- package/dist/runtime/api-CAgKDij7.js.map +0 -1
- package/dist/runtime/descriptor-DCCz5AHd-_nhlnu8U.js.map +0 -1
- package/dist/runtime/id-Bt9XWRGL.js.map +0 -1
- package/dist/runtime/id-E7JKqQ9r.d.ts +0 -1
- package/dist/runtime/pagination-CpYCaRYD-C2Rpp4Cw.d.ts +0 -122
- package/dist/runtime/pagination-E7JKqQ9r.d.ts +0 -1
- package/dist/runtime/result-DKAA4gpS.d.ts +0 -1
- package/dist/runtime/schema-DqogEydG.js.map +0 -1
- package/src/cli/build/client.ts +0 -119
- package/src/cli/build/public.ts +0 -186
- package/src/cli/client-entry.ts +0 -152
- package/src/cli/client-plugin.ts +0 -136
- 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
|
-
|
|
8
|
-
|
|
7
|
+
ControlVersion,
|
|
8
|
+
BeginVersionResponse,
|
|
9
9
|
} from "@ignotum/contracts/control";
|
|
10
10
|
import {
|
|
11
|
-
|
|
11
|
+
BeginVersionResponse as BeginVersionResponseSchema,
|
|
12
12
|
ControlApp as ControlAppSchema,
|
|
13
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
controlUploadHeaderNames,
|
|
20
|
+
controlVersionActivatePath,
|
|
21
|
+
controlVersionFilePath,
|
|
22
|
+
controlVersionFinalizePath,
|
|
23
|
+
controlVersionPath,
|
|
26
24
|
} from "@ignotum/contracts/control";
|
|
27
|
-
import
|
|
28
|
-
import {
|
|
29
|
-
import type { AppId,
|
|
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
|
|
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
|
|
112
|
+
readonly activateVersion: (
|
|
115
113
|
appId: AppId,
|
|
116
|
-
|
|
117
|
-
) => Effect.Effect<
|
|
118
|
-
readonly
|
|
114
|
+
versionId: VersionId,
|
|
115
|
+
) => Effect.Effect<ExecutionSelection, ControlApiError | ControlClientUnavailable>;
|
|
116
|
+
readonly beginVersion: (
|
|
119
117
|
appId: AppId,
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
124
|
+
readonly finalizeVersion: (
|
|
125
|
+
appId: AppId,
|
|
126
|
+
versionId: VersionId,
|
|
127
|
+
) => Effect.Effect<ControlVersion, ControlApiError | ControlClientUnavailable>;
|
|
128
|
+
readonly getVersion: (
|
|
126
129
|
appId: AppId,
|
|
127
|
-
|
|
128
|
-
) => Effect.Effect<
|
|
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
|
|
138
|
+
readonly uploadVersionFile: (
|
|
136
139
|
appId: AppId,
|
|
137
|
-
|
|
138
|
-
file:
|
|
139
|
-
) => Effect.Effect<
|
|
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.
|
|
207
|
-
times:
|
|
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
|
|
266
|
+
const beginVersion = Effect.fn("ControlClient.beginVersion")(function* (
|
|
301
267
|
appId: AppId,
|
|
302
|
-
|
|
268
|
+
versionId: VersionId,
|
|
269
|
+
declaration: UploadDeclaration,
|
|
303
270
|
) {
|
|
304
271
|
const request = yield* HttpClientRequest.post(
|
|
305
|
-
endpoint(config.apiUrl,
|
|
272
|
+
endpoint(config.apiUrl, `${controlVersionPath(appId, versionId)}/begin`),
|
|
306
273
|
).pipe(
|
|
307
|
-
HttpClientRequest.schemaBodyJson(
|
|
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*
|
|
277
|
+
return yield* executeIdempotent(request, BeginVersionResponseSchema);
|
|
313
278
|
});
|
|
314
279
|
|
|
315
|
-
const
|
|
280
|
+
const uploadVersionFile = Effect.fn("ControlClient.uploadVersionFile")(function* (
|
|
316
281
|
appId: AppId,
|
|
317
|
-
|
|
318
|
-
file:
|
|
282
|
+
versionId: VersionId,
|
|
283
|
+
file: VersionUploadFile,
|
|
319
284
|
) {
|
|
320
285
|
const baseRequest = HttpClientRequest.put(
|
|
321
|
-
endpoint(config.apiUrl,
|
|
322
|
-
).pipe(
|
|
323
|
-
|
|
324
|
-
|
|
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(`
|
|
329
|
-
: HttpClientRequest.bodyFile(
|
|
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(`
|
|
300
|
+
unavailable(`Version file '${file.path}' could not be opened.`, cause),
|
|
335
301
|
),
|
|
336
302
|
)
|
|
337
303
|
: Effect.succeed(
|
|
338
|
-
HttpClientRequest.bodyUint8Array(
|
|
304
|
+
HttpClientRequest.bodyUint8Array(uploadRequest, file.bytes, file.contentType),
|
|
339
305
|
);
|
|
340
|
-
const response = yield*
|
|
341
|
-
|
|
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
|
|
318
|
+
const finalizeVersion = Effect.fn("ControlClient.finalizeVersion")(function* (
|
|
345
319
|
appId: AppId,
|
|
346
|
-
|
|
320
|
+
versionId: VersionId,
|
|
347
321
|
) {
|
|
348
322
|
return yield* executeIdempotent(
|
|
349
323
|
HttpClientRequest.post(
|
|
350
|
-
endpoint(config.apiUrl,
|
|
324
|
+
endpoint(config.apiUrl, controlVersionFinalizePath(appId, versionId)),
|
|
351
325
|
),
|
|
352
|
-
|
|
326
|
+
ControlVersionSchema,
|
|
353
327
|
);
|
|
354
328
|
});
|
|
355
329
|
|
|
356
|
-
const
|
|
330
|
+
const activateVersion = Effect.fn("ControlClient.activateVersion")(function* (
|
|
357
331
|
appId: AppId,
|
|
358
|
-
|
|
332
|
+
versionId: VersionId,
|
|
359
333
|
) {
|
|
360
334
|
return yield* executeIdempotent(
|
|
361
335
|
HttpClientRequest.post(
|
|
362
|
-
endpoint(config.apiUrl,
|
|
336
|
+
endpoint(config.apiUrl, controlVersionActivatePath(appId, versionId)),
|
|
363
337
|
),
|
|
364
|
-
|
|
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
|
-
|
|
370
|
-
|
|
353
|
+
activateVersion,
|
|
354
|
+
beginVersion,
|
|
371
355
|
createApp,
|
|
372
|
-
|
|
356
|
+
finalizeVersion,
|
|
373
357
|
getApp,
|
|
358
|
+
getVersion,
|
|
374
359
|
getAppBySlug,
|
|
375
|
-
|
|
360
|
+
uploadVersionFile,
|
|
376
361
|
});
|
|
377
362
|
}),
|
|
378
363
|
);
|
package/src/cli/deploy.ts
CHANGED
|
@@ -1,245 +1,181 @@
|
|
|
1
|
-
import { Effect,
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
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 {
|
|
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
|
-
|
|
9
|
+
ControlClientUnavailable,
|
|
21
10
|
type ControlApiError,
|
|
22
|
-
type
|
|
11
|
+
type VersionUploadFile,
|
|
23
12
|
} from "./control-client.js";
|
|
24
13
|
|
|
25
|
-
export
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
49
|
-
"
|
|
19
|
+
export class VersionActivationUnconfirmed extends Schema.TaggedError<VersionActivationUnconfirmed>()(
|
|
20
|
+
"VersionActivationUnconfirmed",
|
|
50
21
|
{
|
|
51
22
|
cause: Schema.Defect(),
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
30
|
+
version: ControlVersion & { readonly number: VersionNumber },
|
|
68
31
|
cause: ControlApiError | ControlClientUnavailable,
|
|
69
32
|
) =>
|
|
70
33
|
cause._tag === "ControlClientUnavailable" || cause.status >= 500
|
|
71
|
-
?
|
|
34
|
+
? VersionActivationUnconfirmed.make({
|
|
72
35
|
cause,
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
59
|
+
export const uploadVersion = Effect.fn("Deploy.upload")(function* (
|
|
172
60
|
configuration: AppConfiguration,
|
|
173
|
-
build:
|
|
61
|
+
build: ManagedVersionBuildResult,
|
|
174
62
|
) {
|
|
175
63
|
const control = yield* ControlClient;
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
build.
|
|
183
|
-
|
|
184
|
-
|
|
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.
|
|
189
|
-
|
|
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
|
-
|
|
110
|
+
VersionUploadInterrupted.make({
|
|
194
111
|
cause,
|
|
195
|
-
|
|
196
|
-
message: `
|
|
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
|
-
|
|
120
|
+
VersionUploadInterrupted.make({
|
|
205
121
|
cause,
|
|
206
|
-
|
|
207
|
-
message: `
|
|
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
|
-
|
|
213
|
-
.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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*
|
|
147
|
+
const build = yield* buildVersionArtifact(appDirectory);
|
|
224
148
|
const configuration = yield* configureApp(appDirectory, requestedSlug);
|
|
225
|
-
const
|
|
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
|
|
153
|
+
export const resumeVersion = Effect.fn("Deploy.resume")(function* (
|
|
235
154
|
appDirectory: string,
|
|
236
|
-
|
|
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
|
-
.
|
|
243
|
-
.pipe(Effect.
|
|
244
|
-
|
|
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
|
});
|