gencow 0.1.221 → 0.1.223
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/lib/app-command.mjs +47 -17
- package/lib/app-delete-operation.mjs +57 -3
- package/lib/platform-client.mjs +3 -2
- package/lib/request-deadline.mjs +22 -0
- package/lib/static-deploy-command.mjs +55 -18
- package/package.json +3 -3
- package/runtime/server.mjs +136 -105
- package/server/index.js.map +0 -7
package/lib/app-command.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import { pollAppDeleteOperation } from "./app-delete-operation.mjs";
|
|
18
18
|
import { appResponseError } from "./app-response-error.mjs";
|
|
19
19
|
import { readJsonObjectResponse, readJsonValueResponse } from "./http-response-json.mjs";
|
|
20
|
+
import { runWithAbortDeadline } from "./request-deadline.mjs";
|
|
20
21
|
import { BOLD, CYAN, DIM, GREEN, RED, RESET, error, info, log, success, warn } from "./output.mjs";
|
|
21
22
|
import { loadCreds, rpcMutation, rpcQuery, saveCreds, requireCreds } from "./platform-client.mjs";
|
|
22
23
|
import { resolveProjectMetadataPath, resolveProjectSelection } from "./project-context.mjs";
|
|
@@ -27,6 +28,7 @@ import { updateEnvLocalUrl } from "./cli-project-runtime.mjs";
|
|
|
27
28
|
const DEFAULT_DELETE_POLL_ATTEMPTS = 120;
|
|
28
29
|
const DELETE_POLL_INTERVAL_MS = 1_000;
|
|
29
30
|
const DEFAULT_APP_CREATE_TIMEOUT_MS = 60_000;
|
|
31
|
+
const DEFAULT_APP_DELETE_REQUEST_TIMEOUT_MS = 20_000;
|
|
30
32
|
|
|
31
33
|
function resolveAppCreateTimeoutMs(value) {
|
|
32
34
|
const parsed = Number(value);
|
|
@@ -35,6 +37,13 @@ function resolveAppCreateTimeoutMs(value) {
|
|
|
35
37
|
: DEFAULT_APP_CREATE_TIMEOUT_MS;
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
function resolveAppDeleteRequestTimeoutMs(value) {
|
|
41
|
+
const parsed = Number(value);
|
|
42
|
+
return Number.isSafeInteger(parsed) && parsed >= 100 && parsed <= 600_000
|
|
43
|
+
? parsed
|
|
44
|
+
: DEFAULT_APP_DELETE_REQUEST_TIMEOUT_MS;
|
|
45
|
+
}
|
|
46
|
+
|
|
38
47
|
function appResponseStatus(response) {
|
|
39
48
|
return Number.isSafeInteger(response?.status) ? `HTTP ${response.status}` : "unknown HTTP status";
|
|
40
49
|
}
|
|
@@ -137,6 +146,9 @@ async function confirmDelete(name) {
|
|
|
137
146
|
|
|
138
147
|
export function createAppCommand({
|
|
139
148
|
appCreateTimeoutMs = resolveAppCreateTimeoutMs(process.env.GENCOW_APP_CREATE_TIMEOUT_MS),
|
|
149
|
+
appDeleteRequestTimeoutMs = resolveAppDeleteRequestTimeoutMs(
|
|
150
|
+
process.env.GENCOW_APP_DELETE_REQUEST_TIMEOUT_MS,
|
|
151
|
+
),
|
|
140
152
|
clearTimeoutImpl = clearTimeout,
|
|
141
153
|
confirmDeleteImpl = confirmDelete,
|
|
142
154
|
createAbortControllerImpl = () => new AbortController(),
|
|
@@ -159,28 +171,38 @@ export function createAppCommand({
|
|
|
159
171
|
warnImpl = warn,
|
|
160
172
|
}) {
|
|
161
173
|
const boundedAppCreateTimeoutMs = resolveAppCreateTimeoutMs(appCreateTimeoutMs);
|
|
174
|
+
const boundedAppDeleteRequestTimeoutMs = resolveAppDeleteRequestTimeoutMs(appDeleteRequestTimeoutMs);
|
|
162
175
|
|
|
163
176
|
async function requestAppCreate(creds, name) {
|
|
164
|
-
const controller = createAbortControllerImpl();
|
|
165
177
|
const timeoutError = new Error(
|
|
166
178
|
`App creation request timed out after ${boundedAppCreateTimeoutMs}ms. Please try again.`,
|
|
167
179
|
);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
180
|
+
return runWithAbortDeadline({
|
|
181
|
+
clearTimeoutImpl,
|
|
182
|
+
createAbortControllerImpl,
|
|
183
|
+
request: async (signal) => {
|
|
184
|
+
const response = await rpcMutationImpl(creds, "apps.create", { name }, { signal });
|
|
185
|
+
return { response, data: await readJsonObjectResponse(response) };
|
|
186
|
+
},
|
|
187
|
+
setTimeoutImpl,
|
|
188
|
+
timeoutError,
|
|
189
|
+
timeoutMs: boundedAppCreateTimeoutMs,
|
|
174
190
|
});
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function requestAppDelete(creds, name, requestId) {
|
|
194
|
+
const timeoutError = new Error("App deletion request timed out");
|
|
195
|
+
return runWithAbortDeadline({
|
|
196
|
+
clearTimeoutImpl,
|
|
197
|
+
createAbortControllerImpl,
|
|
198
|
+
request: async (signal) => {
|
|
199
|
+
const response = await rpcMutationImpl(creds, "apps.delete", { name, requestId }, { signal });
|
|
178
200
|
return { response, data: await readJsonObjectResponse(response) };
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
201
|
+
},
|
|
202
|
+
setTimeoutImpl,
|
|
203
|
+
timeoutError,
|
|
204
|
+
timeoutMs: boundedAppDeleteRequestTimeoutMs,
|
|
205
|
+
});
|
|
184
206
|
}
|
|
185
207
|
|
|
186
208
|
return async function app(subcmd, ...rest) {
|
|
@@ -342,8 +364,16 @@ ${dashboardLine}
|
|
|
342
364
|
|
|
343
365
|
infoImpl(`Deleting app "${name}"...`);
|
|
344
366
|
const requestId = createDeleteRequestIdImpl();
|
|
345
|
-
|
|
346
|
-
let delData
|
|
367
|
+
let delRes;
|
|
368
|
+
let delData;
|
|
369
|
+
try {
|
|
370
|
+
({ response: delRes, data: delData } = await requestAppDelete(creds, name, requestId));
|
|
371
|
+
} catch {
|
|
372
|
+
errorImpl(
|
|
373
|
+
"App deletion request did not complete. Its operation outcome is unknown; retry the same app delete to inspect it.\nCode: APP_DELETE_REQUEST_UNAVAILABLE",
|
|
374
|
+
);
|
|
375
|
+
return 1;
|
|
376
|
+
}
|
|
347
377
|
if (!isExactAppDeleteResponse(delData, name)) {
|
|
348
378
|
const operationResult = await pollAppDeleteOperation({
|
|
349
379
|
creds,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isExactAppDeleteResponse } from "./app-response-contract.mjs";
|
|
2
2
|
import { APP_DELETE_OPERATION_PATTERN } from "./app-response-error.mjs";
|
|
3
3
|
import { readJsonObjectResponse } from "./http-response-json.mjs";
|
|
4
|
+
import { runWithAbortDeadline } from "./request-deadline.mjs";
|
|
4
5
|
|
|
5
6
|
const APP_DELETE_ACTIVE_STATES = new Set([
|
|
6
7
|
"accepted",
|
|
@@ -26,6 +27,38 @@ const APP_DELETE_RESPONSE_STATES = new Set([
|
|
|
26
27
|
const CORRELATION_ID_PATTERN = /^[A-Za-z0-9_-]{8,128}$/u;
|
|
27
28
|
const DIAGNOSTIC_CODE_PATTERN = /^[A-Za-z][A-Za-z0-9_]{2,127}$/u;
|
|
28
29
|
const APP_ID_PATTERN = /^(?=.{3,63}$)[a-z][a-z0-9]*(?:-[a-z0-9]+){2,7}$/u;
|
|
30
|
+
const DEFAULT_DELETE_OPERATION_TIMEOUT_MS = 120_000;
|
|
31
|
+
const DEFAULT_DELETE_STATUS_REQUEST_TIMEOUT_MS = 10_000;
|
|
32
|
+
|
|
33
|
+
function resolveTimeoutMs(value, fallback) {
|
|
34
|
+
const parsed = Number(value);
|
|
35
|
+
return Number.isSafeInteger(parsed) && parsed >= 1 && parsed <= 600_000 ? parsed : fallback;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function requestDeleteStatus({
|
|
39
|
+
clearTimeoutImpl,
|
|
40
|
+
createAbortControllerImpl,
|
|
41
|
+
creds,
|
|
42
|
+
name,
|
|
43
|
+
operationId,
|
|
44
|
+
requestTimeoutMs,
|
|
45
|
+
rpcQueryImpl,
|
|
46
|
+
setTimeoutImpl,
|
|
47
|
+
}) {
|
|
48
|
+
const timeoutError = new Error("App deletion status request timed out");
|
|
49
|
+
timeoutError.name = "AbortError";
|
|
50
|
+
return runWithAbortDeadline({
|
|
51
|
+
clearTimeoutImpl,
|
|
52
|
+
createAbortControllerImpl,
|
|
53
|
+
request: async (signal) => {
|
|
54
|
+
const statusRes = await rpcQueryImpl(creds, "apps.deleteStatus", { name, operationId }, { signal });
|
|
55
|
+
return { statusRes, statusData: await readJsonObjectResponse(statusRes) };
|
|
56
|
+
},
|
|
57
|
+
setTimeoutImpl,
|
|
58
|
+
timeoutError,
|
|
59
|
+
timeoutMs: requestTimeoutMs,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
29
62
|
|
|
30
63
|
function isAllowedStatusPath(value, operationId, expectedAppId) {
|
|
31
64
|
if (value === undefined) return true;
|
|
@@ -104,12 +137,18 @@ function isMatchingTerminalFailureResponse(value, accepted, expectedAppId) {
|
|
|
104
137
|
}
|
|
105
138
|
|
|
106
139
|
export async function pollAppDeleteOperation({
|
|
140
|
+
clearTimeoutImpl = clearTimeout,
|
|
141
|
+
createAbortControllerImpl = () => new AbortController(),
|
|
107
142
|
creds,
|
|
108
143
|
initialBody,
|
|
109
144
|
name,
|
|
145
|
+
nowImpl = Date.now,
|
|
146
|
+
operationTimeoutMs = DEFAULT_DELETE_OPERATION_TIMEOUT_MS,
|
|
110
147
|
pollAttempts,
|
|
111
148
|
pollIntervalMs,
|
|
149
|
+
requestTimeoutMs = DEFAULT_DELETE_STATUS_REQUEST_TIMEOUT_MS,
|
|
112
150
|
rpcQueryImpl,
|
|
151
|
+
setTimeoutImpl = setTimeout,
|
|
113
152
|
sleepImpl,
|
|
114
153
|
}) {
|
|
115
154
|
const accepted = parseAppDeleteOperationEnvelope(initialBody, name);
|
|
@@ -121,13 +160,25 @@ export async function pollAppDeleteOperation({
|
|
|
121
160
|
let latest = accepted;
|
|
122
161
|
let successfulPollResponses = 0;
|
|
123
162
|
let transportFailures = 0;
|
|
163
|
+
const deadlineAt = nowImpl() + resolveTimeoutMs(operationTimeoutMs, DEFAULT_DELETE_OPERATION_TIMEOUT_MS);
|
|
164
|
+
const boundedRequestTimeoutMs = resolveTimeoutMs(
|
|
165
|
+
requestTimeoutMs,
|
|
166
|
+
DEFAULT_DELETE_STATUS_REQUEST_TIMEOUT_MS,
|
|
167
|
+
);
|
|
124
168
|
for (let attempt = 0; attempt < pollAttempts; attempt += 1) {
|
|
169
|
+
const remainingMs = deadlineAt - nowImpl();
|
|
170
|
+
if (remainingMs <= 0) break;
|
|
125
171
|
try {
|
|
126
|
-
const statusRes = await
|
|
172
|
+
const { statusData, statusRes } = await requestDeleteStatus({
|
|
173
|
+
clearTimeoutImpl,
|
|
174
|
+
createAbortControllerImpl,
|
|
175
|
+
creds,
|
|
127
176
|
name,
|
|
128
177
|
operationId: accepted.operationId,
|
|
178
|
+
requestTimeoutMs: Math.min(boundedRequestTimeoutMs, remainingMs),
|
|
179
|
+
rpcQueryImpl,
|
|
180
|
+
setTimeoutImpl,
|
|
129
181
|
});
|
|
130
|
-
const statusData = await readJsonObjectResponse(statusRes);
|
|
131
182
|
// A lifecycle operation is durable. A transient 5xx while the status
|
|
132
183
|
// query is being served must not turn that durable receipt into a client
|
|
133
184
|
// contract failure or abandon the operation's correlation identity.
|
|
@@ -157,7 +208,10 @@ export async function pollAppDeleteOperation({
|
|
|
157
208
|
transportFailures += 1;
|
|
158
209
|
// The durable receipt remains authoritative across transient polling failures.
|
|
159
210
|
}
|
|
160
|
-
if (attempt + 1 < pollAttempts)
|
|
211
|
+
if (attempt + 1 < pollAttempts) {
|
|
212
|
+
const remainingAfterRequestMs = deadlineAt - nowImpl();
|
|
213
|
+
if (remainingAfterRequestMs > 0) await sleepImpl(Math.min(pollIntervalMs, remainingAfterRequestMs));
|
|
214
|
+
}
|
|
161
215
|
}
|
|
162
216
|
|
|
163
217
|
const pollingUnavailable = successfulPollResponses === 0 && transportFailures > 0;
|
package/lib/platform-client.mjs
CHANGED
|
@@ -96,10 +96,11 @@ export async function platformFetch(creds, path, opts = {}) {
|
|
|
96
96
|
});
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
export async function rpcQuery(creds, queryName, args = {}) {
|
|
99
|
+
export async function rpcQuery(creds, queryName, args = {}, requestOptions = {}) {
|
|
100
100
|
return platformFetch(creds, "/api/query", {
|
|
101
|
+
...requestOptions,
|
|
101
102
|
method: "POST",
|
|
102
|
-
headers: { "Content-Type": "application/json" },
|
|
103
|
+
headers: { ...requestOptions.headers, "Content-Type": "application/json" },
|
|
103
104
|
body: JSON.stringify({ name: queryName, args }),
|
|
104
105
|
});
|
|
105
106
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export async function runWithAbortDeadline({
|
|
2
|
+
clearTimeoutImpl = clearTimeout,
|
|
3
|
+
createAbortControllerImpl = () => new AbortController(),
|
|
4
|
+
request,
|
|
5
|
+
setTimeoutImpl = setTimeout,
|
|
6
|
+
timeoutError,
|
|
7
|
+
timeoutMs,
|
|
8
|
+
}) {
|
|
9
|
+
const controller = createAbortControllerImpl();
|
|
10
|
+
let timeoutHandle;
|
|
11
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
12
|
+
timeoutHandle = setTimeoutImpl(() => {
|
|
13
|
+
controller.abort(timeoutError);
|
|
14
|
+
reject(timeoutError);
|
|
15
|
+
}, timeoutMs);
|
|
16
|
+
});
|
|
17
|
+
try {
|
|
18
|
+
return await Promise.race([request(controller.signal), deadline]);
|
|
19
|
+
} finally {
|
|
20
|
+
clearTimeoutImpl(timeoutHandle);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -21,8 +21,38 @@ import { pollAcceptedDeploymentOperation } from "./deployment-operation-poll.mjs
|
|
|
21
21
|
import { writeProjectMetadata } from "./deploy-project-metadata.mjs";
|
|
22
22
|
|
|
23
23
|
function shouldPreserveFinalizeAttempt(error) {
|
|
24
|
-
return error?.retryable === true ||
|
|
25
|
-
|
|
24
|
+
return error?.retryable === true || error?.message === "APP_SERVING_ROUTE_COMMIT_PENDING";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const TRANSIENT_STATIC_DEPLOY_PROXY_STATUSES = new Set([502, 503, 504]);
|
|
28
|
+
|
|
29
|
+
async function isBodylessTransientStaticDeployResponse(response) {
|
|
30
|
+
if (
|
|
31
|
+
!TRANSIENT_STATIC_DEPLOY_PROXY_STATUSES.has(response?.status) ||
|
|
32
|
+
typeof response?.clone !== "function"
|
|
33
|
+
) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
return (await readJsonObjectResponse(response.clone())) === null;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function postStaticDeployWithTransientProxyRetry({
|
|
44
|
+
send,
|
|
45
|
+
retryDelaysMs = [250, 500, 1_000],
|
|
46
|
+
delayImpl = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
47
|
+
}) {
|
|
48
|
+
let response;
|
|
49
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
50
|
+
response = await send();
|
|
51
|
+
if (!(await isBodylessTransientStaticDeployResponse(response)) || attempt >= retryDelaysMs.length) {
|
|
52
|
+
return response;
|
|
53
|
+
}
|
|
54
|
+
await delayImpl(retryDelaysMs[attempt]);
|
|
55
|
+
}
|
|
26
56
|
}
|
|
27
57
|
|
|
28
58
|
export function resolveStaticDeployDir({
|
|
@@ -178,6 +208,8 @@ export function createStaticDeployRuntime({
|
|
|
178
208
|
resolvePathImpl = resolve,
|
|
179
209
|
setTimeoutImpl = setTimeout,
|
|
180
210
|
statSyncImpl = statSync,
|
|
211
|
+
staticDeployRetryDelayImpl = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
212
|
+
staticDeployRetryDelaysMs,
|
|
181
213
|
successImpl = success,
|
|
182
214
|
tarCreateImpl,
|
|
183
215
|
unlinkSyncImpl = unlinkSync,
|
|
@@ -383,22 +415,27 @@ export function createStaticDeployRuntime({
|
|
|
383
415
|
infoImpl("Deploying static files...");
|
|
384
416
|
let deployRes;
|
|
385
417
|
try {
|
|
386
|
-
deployRes = await
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
418
|
+
deployRes = await postStaticDeployWithTransientProxyRetry({
|
|
419
|
+
send: () =>
|
|
420
|
+
platformFetchImpl(creds, `/platform/apps/${appId}/deploy-static`, {
|
|
421
|
+
method: "POST",
|
|
422
|
+
headers: {
|
|
423
|
+
"Content-Type": "application/octet-stream",
|
|
424
|
+
"X-Deploy-Local-Dir": cwd,
|
|
425
|
+
"X-Gencow-Deploy-Claim": deployClaim.header,
|
|
426
|
+
"X-Gencow-Deploy-Protocol": "1",
|
|
427
|
+
"X-Gencow-CLI-Version": deployClaim.claim.generatedBy.version,
|
|
428
|
+
...(releaseAttemptId
|
|
429
|
+
? {
|
|
430
|
+
"X-Gencow-Release-Attempt-Id": releaseAttemptId,
|
|
431
|
+
"X-Gencow-Release-Environment": opts.envTarget || "dev",
|
|
432
|
+
}
|
|
433
|
+
: {}),
|
|
434
|
+
},
|
|
435
|
+
body: bundleBuffer,
|
|
436
|
+
}),
|
|
437
|
+
retryDelaysMs: staticDeployRetryDelaysMs,
|
|
438
|
+
delayImpl: staticDeployRetryDelayImpl,
|
|
402
439
|
});
|
|
403
440
|
if (deployRes.status === 202) {
|
|
404
441
|
deployRes = await pollAcceptedDeploymentOperation({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gencow",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.223",
|
|
4
4
|
"description": "Gencow — AI Backend Engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/node": "^25.9.5",
|
|
35
35
|
"better-auth": "^1.6.23",
|
|
36
|
-
"@gencow/core": "0.1.43",
|
|
37
36
|
"@gencow/client": "0.2.7",
|
|
38
|
-
"@gencow/react": "0.2.7"
|
|
37
|
+
"@gencow/react": "0.2.7",
|
|
38
|
+
"@gencow/core": "0.1.43"
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
41
|
"prebuild": "pnpm --filter @gencow/migration-contract run build && pnpm --filter @gencow/server run build",
|