@fluid-app/fluid-cli-mist 0.1.49 → 0.1.51

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/dist/index.mjs CHANGED
@@ -235,8 +235,7 @@ async function listBranchEnvironments(client, slug) {
235
235
  * the key before calling and retries with it.
236
236
  */
237
237
  async function requestStaging(client, slug, request) {
238
- if (!SOURCE_REVISION.test(request.sourceRevision)) throw new Error("Source revision must be a full 40-character commit SHA");
239
- if (!OPERATION_KEY.test(request.operationKey)) throw new Error("Operation key must be 1-128 of A-Z a-z 0-9 . _ : -");
238
+ checkStagingRequest(request.sourceRevision, request.operationKey);
240
239
  const id = await resolveMistId(client, slug);
241
240
  return parseOperationEnvelope(await client.post(`${base(id)}/branch-environments/staging`, { source_revision: request.sourceRevision }, { "X-Operation-Key": request.operationKey }));
242
241
  }
@@ -245,6 +244,20 @@ async function fetchBranchOperation(client, slug, operationId) {
245
244
  const id = await resolveMistId(client, slug);
246
245
  return parseOperationEnvelope(await client.get(`${base(id)}/branch-operations/${encodeURIComponent(operationId)}`));
247
246
  }
247
+ /**
248
+ * Refuse a revision or operation key the server would reject, before
249
+ * anything is sent or saved.
250
+ *
251
+ * Raises Error naming the malformed value.
252
+ */
253
+ function checkStagingRequest(sourceRevision, operationKey) {
254
+ if (!SOURCE_REVISION.test(sourceRevision)) throw new Error("Source revision must be a full 40-character commit SHA");
255
+ if (operationKey !== void 0 && !isOperationKey(operationKey)) throw new Error("Operation key must be 1-128 of A-Z a-z 0-9 . _ : -");
256
+ }
257
+ /** Whether the server would accept `key` as an operation key. */
258
+ function isOperationKey(key) {
259
+ return OPERATION_KEY.test(key);
260
+ }
248
261
  /** A fresh key for a new staging request. */
249
262
  function newOperationKey() {
250
263
  return `staging-${randomUUID()}`;
@@ -4224,6 +4237,110 @@ const stagingStatusCommand = new Command("status").description("Show the staging
4224
4237
  console.log(capability.staging.available ? "Staging can be requested with `fluid mist staging request --revision <sha>`." : chalk.yellow(describeUnavailable(capability.staging)));
4225
4238
  }));
4226
4239
  //#endregion
4240
+ //#region src/lib/staging-request-store.ts
4241
+ /**
4242
+ * Staging requests the server hasn't confirmed yet.
4243
+ *
4244
+ * `fluid mist staging request` saves its operation key here before it sends
4245
+ * the request and removes it once the server answers with an operation. If
4246
+ * the CLI dies in between (crash, lost connection, closed terminal), the
4247
+ * next request for the same revision reuses the saved key, so the server
4248
+ * resumes the same operation instead of starting a second one. After the
4249
+ * server has confirmed, `fluid mist staging wait` follows the operation.
4250
+ *
4251
+ * Records live under the Fluid config dir (`~/.fluid/mist/staging-requests`)
4252
+ * and hold only ids, a commit SHA and a timestamp, no tokens or secrets.
4253
+ */
4254
+ function createFileStagingRequestStore(dir = join(getConfigDir(), "mist", "staging-requests")) {
4255
+ const fileFor = (mistId) => join(dir, `${encodeURIComponent(mistId)}.json`);
4256
+ return {
4257
+ save(request) {
4258
+ mkdirSync(dir, {
4259
+ recursive: true,
4260
+ mode: 448
4261
+ });
4262
+ writeFileSync(fileFor(request.mistId), JSON.stringify(request, null, 2) + "\n", {
4263
+ encoding: "utf8",
4264
+ mode: 384
4265
+ });
4266
+ },
4267
+ load(mistId) {
4268
+ try {
4269
+ const parsed = JSON.parse(readFileSync(fileFor(mistId), "utf8"));
4270
+ return isPendingRequest(parsed) && parsed.mistId === mistId ? parsed : null;
4271
+ } catch {
4272
+ return null;
4273
+ }
4274
+ },
4275
+ remove(mistId) {
4276
+ rmSync(fileFor(mistId), { force: true });
4277
+ }
4278
+ };
4279
+ }
4280
+ /**
4281
+ * The key to send for `sourceRevision`: the saved one when an unconfirmed
4282
+ * request for the same revision is pending, otherwise none. A pending
4283
+ * request for another revision, or with a key the server would refuse, is
4284
+ * dropped; if the server did admit it, the capability check reports it as
4285
+ * the run in progress.
4286
+ */
4287
+ function pendingKeyFor(store, mistId, sourceRevision) {
4288
+ const pending = store.load(mistId);
4289
+ if (!pending) return null;
4290
+ if (pending.sourceRevision === sourceRevision && isOperationKey(pending.operationKey)) return pending.operationKey;
4291
+ store.remove(mistId);
4292
+ return null;
4293
+ }
4294
+ function isPendingRequest(value) {
4295
+ if (typeof value !== "object" || value === null) return false;
4296
+ const record = value;
4297
+ return typeof record["mistId"] === "string" && typeof record["operationKey"] === "string" && typeof record["sourceRevision"] === "string" && typeof record["requestedAt"] === "string";
4298
+ }
4299
+ /**
4300
+ * Request staging, remembering the key until the server answers. A saved
4301
+ * key for the same revision is reused, so a request the CLI never heard
4302
+ * back about resumes the same operation. The record is removed once the
4303
+ * server answers, whether it admitted the request or refused it, and kept
4304
+ * when the request may never have reached it.
4305
+ */
4306
+ async function requestStagingResumably(client, slug, request) {
4307
+ const { store, sourceRevision } = request;
4308
+ checkStagingRequest(sourceRevision, request.operationKey);
4309
+ const mistId = await resolveMistId(client, slug);
4310
+ const savedKey = request.operationKey ? null : pendingKeyFor(store, mistId, sourceRevision);
4311
+ if (savedKey) request.onResume?.(savedKey);
4312
+ try {
4313
+ const operation = await admitStaging(client, slug, {
4314
+ sourceRevision,
4315
+ operationKey: request.operationKey ?? savedKey ?? void 0,
4316
+ onOperationKey: (key) => {
4317
+ store.save({
4318
+ mistId,
4319
+ operationKey: key,
4320
+ sourceRevision,
4321
+ requestedAt: (/* @__PURE__ */ new Date()).toISOString()
4322
+ });
4323
+ request.onOperationKey?.(key);
4324
+ }
4325
+ });
4326
+ store.remove(mistId);
4327
+ return operation;
4328
+ } catch (err) {
4329
+ if (serverAnswered(err)) store.remove(mistId);
4330
+ throw err;
4331
+ }
4332
+ }
4333
+ /**
4334
+ * Whether the server's answer settled the request. A network failure, a
4335
+ * rate limit, a server error or a response the CLI can't read may come
4336
+ * after the operation was recorded, so the key is kept for those and the
4337
+ * next request replays it.
4338
+ */
4339
+ function serverAnswered(err) {
4340
+ if (err instanceof MistApiError) return err.status !== 0 && err.status !== 429 && err.status < 500;
4341
+ return err instanceof StagingOperationFailedError;
4342
+ }
4343
+ //#endregion
4227
4344
  //#region src/commands/staging/wait.ts
4228
4345
  /** `fluid mist staging wait` — follow a staging operation to the end. */
4229
4346
  const POLL_INTERVAL_MS = 5e3;
@@ -4256,9 +4373,11 @@ const stagingRequestCommand = new Command("request").description("Request the st
4256
4373
  const resolved = resolveSlug(slug);
4257
4374
  const client = createMistClient();
4258
4375
  const sourceRevision = opts.revision ?? await localRevision(client, resolved);
4259
- let operation = await admitStaging(client, resolved, {
4376
+ let operation = await requestStagingResumably(client, resolved, {
4260
4377
  sourceRevision,
4261
4378
  operationKey: opts.operationKey,
4379
+ store: createFileStagingRequestStore(),
4380
+ onResume: (key) => process.stderr.write(chalk.dim(`Resuming the unconfirmed request for ${sourceRevision.slice(0, 7)} (key ${key}).\n`)),
4262
4381
  onOperationKey: (key) => process.stderr.write(chalk.dim(`Operation key ${key}. If this is interrupted, rerun with --operation-key ${key} --revision ${sourceRevision}.\n`))
4263
4382
  });
4264
4383
  process.stderr.write(`Staging operation ${operation.id}: ${describeProgress(operation)}\n`);