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.
@@ -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
- let timeoutHandle;
169
- const timeoutPromise = new Promise((_resolve, reject) => {
170
- timeoutHandle = setTimeoutImpl(() => {
171
- controller.abort(timeoutError);
172
- reject(timeoutError);
173
- }, boundedAppCreateTimeoutMs);
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
- try {
176
- const request = (async () => {
177
- const response = await rpcMutationImpl(creds, "apps.create", { name }, { signal: controller.signal });
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
- return await Promise.race([request, timeoutPromise]);
181
- } finally {
182
- clearTimeoutImpl(timeoutHandle);
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
- const delRes = await rpcMutationImpl(creds, "apps.delete", { name, requestId });
346
- let delData = await readJsonObjectResponse(delRes);
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 rpcQueryImpl(creds, "apps.deleteStatus", {
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) await sleepImpl(pollIntervalMs);
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;
@@ -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
- error?.message === "APP_SERVING_ROUTE_COMMIT_PENDING";
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 platformFetchImpl(creds, `/platform/apps/${appId}/deploy-static`, {
387
- method: "POST",
388
- headers: {
389
- "Content-Type": "application/octet-stream",
390
- "X-Deploy-Local-Dir": cwd,
391
- "X-Gencow-Deploy-Claim": deployClaim.header,
392
- "X-Gencow-Deploy-Protocol": "1",
393
- "X-Gencow-CLI-Version": deployClaim.claim.generatedBy.version,
394
- ...(releaseAttemptId
395
- ? {
396
- "X-Gencow-Release-Attempt-Id": releaseAttemptId,
397
- "X-Gencow-Release-Environment": opts.envTarget || "dev",
398
- }
399
- : {}),
400
- },
401
- body: bundleBuffer,
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.221",
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",