@xfey/tutti 0.1.53 → 0.1.54
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/artifacts/index.d.ts +3 -3
- package/dist/artifacts/index.js +3 -3
- package/dist/artifacts/live-manifest.d.ts +23 -0
- package/dist/artifacts/live-manifest.js +115 -0
- package/dist/artifacts/preview-runtime.d.ts +34 -36
- package/dist/artifacts/preview-runtime.js +215 -273
- package/dist/artifacts/static-preview.d.ts +2 -1
- package/dist/artifacts/static-preview.js +19 -21
- package/dist/control-plane/index.d.ts +1 -0
- package/dist/control-plane/index.js +13 -2
- package/dist/control-plane/types.d.ts +4 -0
- package/dist/server-shell/cli/host-server-runtime.js +20 -0
- package/dist/server-shell/http/routes/project-api/artifacts-routes.js +43 -110
- package/dist/server-shell/http/routes/project-api/openapi-artifacts-routes.d.ts +5 -8
- package/dist/server-shell/http/routes/project-api/openapi-artifacts-routes.js +5 -33
- package/dist/server-shell/http/routes/project-api/openapi.d.ts +17 -8
- package/dist/server-shell/http/routes/project-api/types.d.ts +3 -2
- package/node_modules/@tutti/shared/dist/schemas/api/artifacts.d.ts +7 -88
- package/node_modules/@tutti/shared/dist/schemas/api/artifacts.js +3 -48
- package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -1
- package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -1
- package/node_modules/@tutti/shared/dist/schemas/api/types.d.ts +1 -10
- package/package.json +1 -1
- package/web/assets/{homepage-motion-scene-F4ibPvI9.js → homepage-motion-scene-C96Na8TV.js} +1 -1
- package/web/assets/{index-B1mBtL9x.js → index-CM41hrQd.js} +3 -3
- package/web/assets/index-nWy1QP97.css +1 -0
- package/web/assets/tutti_avatar-_0Yc-cMJ.png +0 -0
- package/web/index.html +2 -2
- package/dist/artifacts/manifest.d.ts +0 -41
- package/dist/artifacts/manifest.js +0 -169
- package/web/assets/index-OFlcQ3eS.css +0 -1
- package/web/assets/tutti_avatar-BBhaZGi3.png +0 -0
|
@@ -2,11 +2,12 @@ import { lstatSync, realpathSync } from "node:fs";
|
|
|
2
2
|
import { isAbsolute, relative, resolve } from "node:path";
|
|
3
3
|
import { redactAndTruncateText } from "@tutti/shared/utils";
|
|
4
4
|
import { WorkspaceOpsError } from "../workspace-ops/errors.js";
|
|
5
|
-
import {
|
|
5
|
+
import { readLiveArtifactDeclaration, } from "./live-manifest.js";
|
|
6
6
|
import { allocateArtifactPreviewPort, ArtifactDiagnosticBuffer, cleanupArtifactProcessRuntime, createArtifactPreviewEnv, createArtifactProcessRuntime, spawnArtifactPreviewProcess, terminateArtifactProcessTree, } from "./process-boundary.js";
|
|
7
|
-
import { rewriteArtifactPreviewResourceUrls } from "./static-preview.js";
|
|
7
|
+
import { readArtifactStaticPreview, rewriteArtifactPreviewResourceUrls, } from "./static-preview.js";
|
|
8
8
|
export const ARTIFACT_PREVIEW_REQUEST_BODY_LIMIT_BYTES = 1024 * 1024;
|
|
9
9
|
export const ARTIFACT_PREVIEW_RESPONSE_BODY_LIMIT_BYTES = 5 * 1024 * 1024;
|
|
10
|
+
export const ARTIFACT_PREVIEW_BASE_PATH = "/artifacts/preview";
|
|
10
11
|
const DEFAULT_READY_TIMEOUT_MS = 20_000;
|
|
11
12
|
const DEFAULT_READY_PROBE_INTERVAL_MS = 250;
|
|
12
13
|
const DEFAULT_READY_PROBE_REQUEST_TIMEOUT_MS = 2_000;
|
|
@@ -44,64 +45,54 @@ const RESPONSE_HEADER_DENYLIST = new Set([
|
|
|
44
45
|
"www-authenticate",
|
|
45
46
|
]);
|
|
46
47
|
const SENSITIVE_RESPONSE_HEADER_PATTERN = /authorization|cookie|credential|proxy-auth|secret|token|x-api[-_]?key/iu;
|
|
47
|
-
function isServerDeclaration(declaration) {
|
|
48
|
-
return declaration.status === "declared" && declaration.runtime.kind === "server";
|
|
49
|
-
}
|
|
50
|
-
function serverDeclarationKey(declaration) {
|
|
51
|
-
return [
|
|
52
|
-
declaration.snapshot_oid,
|
|
53
|
-
declaration.manifest_blob_oid,
|
|
54
|
-
declaration.runtime.cwd_path,
|
|
55
|
-
declaration.runtime.entry_path,
|
|
56
|
-
].join(":");
|
|
57
|
-
}
|
|
58
48
|
function buildServerPreviewUrl(options) {
|
|
59
49
|
const basePath = (options.basePath ?? ARTIFACT_PREVIEW_BASE_PATH).replace(/\/+$/u, "");
|
|
60
50
|
const normalizedEntryPath = options.entryPath === "/" ? "/" : `/${options.entryPath.replace(/^\/+/, "")}`;
|
|
61
51
|
const separator = normalizedEntryPath.includes("?") ? "&" : "?";
|
|
62
|
-
return `${basePath}${normalizedEntryPath}${separator}
|
|
52
|
+
return `${basePath}${normalizedEntryPath}${separator}g=${encodeURIComponent(options.generation)}`;
|
|
63
53
|
}
|
|
64
|
-
function
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
};
|
|
54
|
+
function buildStaticPreviewUrl(options) {
|
|
55
|
+
const basePath = (options.basePath ?? ARTIFACT_PREVIEW_BASE_PATH).replace(/\/+$/u, "");
|
|
56
|
+
const encodedEntryPath = options.entryPath
|
|
57
|
+
.split("/")
|
|
58
|
+
.filter((segment) => segment.length > 0)
|
|
59
|
+
.map((segment) => encodeURIComponent(segment))
|
|
60
|
+
.join("/");
|
|
61
|
+
const path = encodedEntryPath.length === 0 ? basePath : `${basePath}/${encodedEntryPath}`;
|
|
62
|
+
return `${path}?g=${encodeURIComponent(options.generation)}`;
|
|
74
63
|
}
|
|
75
|
-
function
|
|
64
|
+
function projectionWithState(options) {
|
|
76
65
|
const artifact = options.declaration.manifest.artifact;
|
|
77
66
|
const baseProjection = {
|
|
78
67
|
status: "declared",
|
|
79
|
-
|
|
80
|
-
manifest_blob_oid: options.declaration.manifest_blob_oid,
|
|
68
|
+
preview_generation: options.state.generation,
|
|
81
69
|
artifact: {
|
|
82
70
|
title: artifact.title,
|
|
83
|
-
kind:
|
|
71
|
+
kind: artifact.kind,
|
|
84
72
|
network: artifact.network,
|
|
85
73
|
},
|
|
86
74
|
preview: {
|
|
87
|
-
status: "
|
|
75
|
+
status: "idle",
|
|
88
76
|
},
|
|
89
77
|
};
|
|
90
78
|
const state = options.state;
|
|
91
|
-
if (state === undefined || state.key !== serverDeclarationKey(options.declaration)) {
|
|
92
|
-
return baseProjection;
|
|
93
|
-
}
|
|
94
79
|
if (state.status === "ready") {
|
|
80
|
+
const url = artifact.kind === "static" && options.declaration.runtime.kind === "static"
|
|
81
|
+
? buildStaticPreviewUrl({
|
|
82
|
+
basePath: options.previewBasePath,
|
|
83
|
+
entryPath: options.declaration.runtime.entry_path,
|
|
84
|
+
generation: state.generation,
|
|
85
|
+
})
|
|
86
|
+
: buildServerPreviewUrl({
|
|
87
|
+
basePath: options.previewBasePath,
|
|
88
|
+
entryPath: state.entryPath,
|
|
89
|
+
generation: state.generation,
|
|
90
|
+
});
|
|
95
91
|
return {
|
|
96
92
|
...baseProjection,
|
|
97
93
|
preview: {
|
|
98
94
|
status: "ready",
|
|
99
|
-
url
|
|
100
|
-
basePath: options.previewBasePath,
|
|
101
|
-
entryPath: state.entryPath,
|
|
102
|
-
manifestBlobOid: state.manifestBlobOid,
|
|
103
|
-
instanceId: state.instanceId,
|
|
104
|
-
}),
|
|
95
|
+
url,
|
|
105
96
|
},
|
|
106
97
|
};
|
|
107
98
|
}
|
|
@@ -215,8 +206,7 @@ function safeRedirectLocation(options) {
|
|
|
215
206
|
if (redirected.origin !== options.localUrl.origin) {
|
|
216
207
|
throw new WorkspaceOpsError("validation_failed", "Artifact preview redirect left the managed preview instance");
|
|
217
208
|
}
|
|
218
|
-
redirected.searchParams.set("
|
|
219
|
-
redirected.searchParams.set("preview", options.state.instanceId);
|
|
209
|
+
redirected.searchParams.set("g", options.state.generation);
|
|
220
210
|
return `${ARTIFACT_PREVIEW_BASE_PATH}${redirected.pathname}${redirected.search}${redirected.hash}`;
|
|
221
211
|
}
|
|
222
212
|
function unrefTimer(timer) {
|
|
@@ -237,7 +227,6 @@ export function isArtifactPreviewHttpMethod(method) {
|
|
|
237
227
|
method === "OPTIONS");
|
|
238
228
|
}
|
|
239
229
|
export class ArtifactPreviewManager {
|
|
240
|
-
now;
|
|
241
230
|
baseEnv;
|
|
242
231
|
spawnProcess;
|
|
243
232
|
terminateProcess;
|
|
@@ -257,10 +246,14 @@ export class ArtifactPreviewManager {
|
|
|
257
246
|
responseBodyLimitBytes;
|
|
258
247
|
idleStopTtlMs;
|
|
259
248
|
pendingTasks = new Set();
|
|
249
|
+
leases = new Map();
|
|
250
|
+
leaseSweepTimer;
|
|
260
251
|
state;
|
|
261
|
-
|
|
252
|
+
declaration;
|
|
253
|
+
lastProjection = { status: "not_declared" };
|
|
254
|
+
workspaceUpdating = false;
|
|
255
|
+
nextGenerationSequence = 0;
|
|
262
256
|
constructor(options = {}) {
|
|
263
|
-
this.now = options.now ?? (() => new Date());
|
|
264
257
|
this.baseEnv = options.baseEnv ?? process.env;
|
|
265
258
|
this.spawnProcess = options.spawnProcess ?? spawnArtifactPreviewProcess;
|
|
266
259
|
this.terminateProcess = options.terminateProcess ?? terminateArtifactProcessTree;
|
|
@@ -283,187 +276,40 @@ export class ArtifactPreviewManager {
|
|
|
283
276
|
options.responseBodyLimitBytes ?? ARTIFACT_PREVIEW_RESPONSE_BODY_LIMIT_BYTES;
|
|
284
277
|
this.idleStopTtlMs = options.idleStopTtlMs ?? DEFAULT_IDLE_STOP_TTL_MS;
|
|
285
278
|
}
|
|
286
|
-
readProjection(options) {
|
|
287
|
-
|
|
288
|
-
workspaceRoot: options.workspaceRoot
|
|
289
|
-
previewBasePath: options.previewBasePath,
|
|
290
|
-
now: this.now,
|
|
291
|
-
});
|
|
292
|
-
this.stopStaleRuntime(declaration);
|
|
293
|
-
return {
|
|
294
|
-
artifact: isServerDeclaration(declaration)
|
|
295
|
-
? projectionWithServerState({
|
|
296
|
-
declaration,
|
|
297
|
-
state: this.state,
|
|
298
|
-
previewBasePath: options.previewBasePath,
|
|
299
|
-
})
|
|
300
|
-
: declaration.projection,
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
start(options) {
|
|
304
|
-
const declaration = readArtifactDeclaration({
|
|
305
|
-
workspaceRoot: options.workspaceRoot,
|
|
306
|
-
previewBasePath: options.previewBasePath,
|
|
307
|
-
now: this.now,
|
|
308
|
-
});
|
|
309
|
-
this.stopStaleRuntime(declaration);
|
|
310
|
-
if (declaration.status === "not_declared") {
|
|
311
|
-
return {
|
|
312
|
-
disposition: { kind: "not_declared" },
|
|
313
|
-
result: { artifact: declaration.projection },
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
if (declaration.status === "invalid") {
|
|
317
|
-
return {
|
|
318
|
-
disposition: declarationInvalidDisposition(declaration).start,
|
|
319
|
-
result: { artifact: declaration.projection },
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
if (!isServerDeclaration(declaration)) {
|
|
323
|
-
return {
|
|
324
|
-
disposition: { kind: "unsupported" },
|
|
325
|
-
result: { artifact: declaration.projection },
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
const key = serverDeclarationKey(declaration);
|
|
329
|
-
if (this.state?.key === key) {
|
|
330
|
-
if (this.state.status === "ready") {
|
|
331
|
-
this.recordHeartbeat(this.state);
|
|
332
|
-
return {
|
|
333
|
-
disposition: { kind: "already_running" },
|
|
334
|
-
result: {
|
|
335
|
-
artifact: projectionWithServerState({
|
|
336
|
-
declaration,
|
|
337
|
-
state: this.state,
|
|
338
|
-
previewBasePath: options.previewBasePath,
|
|
339
|
-
}),
|
|
340
|
-
},
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
if (this.state.status === "starting") {
|
|
344
|
-
this.recordHeartbeat(this.state);
|
|
345
|
-
return {
|
|
346
|
-
disposition: { kind: "starting" },
|
|
347
|
-
result: {
|
|
348
|
-
artifact: projectionWithServerState({
|
|
349
|
-
declaration,
|
|
350
|
-
state: this.state,
|
|
351
|
-
previewBasePath: options.previewBasePath,
|
|
352
|
-
}),
|
|
353
|
-
},
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
this.stopCurrentProcess();
|
|
279
|
+
readProjection(options = {}) {
|
|
280
|
+
if (this.declaration === undefined && options.workspaceRoot !== undefined) {
|
|
281
|
+
this.initializeWorkspace({ workspaceRoot: options.workspaceRoot });
|
|
357
282
|
}
|
|
358
|
-
this.startProcess(options.workspaceRoot, declaration);
|
|
359
283
|
return {
|
|
360
|
-
|
|
361
|
-
result: {
|
|
362
|
-
artifact: projectionWithServerState({
|
|
363
|
-
declaration,
|
|
364
|
-
state: this.state,
|
|
365
|
-
previewBasePath: options.previewBasePath,
|
|
366
|
-
}),
|
|
367
|
-
},
|
|
284
|
+
artifact: this.currentProjection(options.previewBasePath),
|
|
368
285
|
};
|
|
369
286
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
previewBasePath: options.previewBasePath,
|
|
374
|
-
now: this.now,
|
|
375
|
-
});
|
|
376
|
-
this.stopStaleRuntime(declaration);
|
|
377
|
-
if (declaration.status === "not_declared") {
|
|
378
|
-
return {
|
|
379
|
-
disposition: { kind: "not_declared" },
|
|
380
|
-
result: { artifact: declaration.projection },
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
if (declaration.status === "invalid") {
|
|
384
|
-
return {
|
|
385
|
-
disposition: declarationInvalidDisposition(declaration).heartbeat,
|
|
386
|
-
result: { artifact: declaration.projection },
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
if (!isServerDeclaration(declaration) ||
|
|
390
|
-
this.state?.key !== serverDeclarationKey(declaration)) {
|
|
391
|
-
return {
|
|
392
|
-
disposition: { kind: "not_running" },
|
|
393
|
-
result: {
|
|
394
|
-
artifact: isServerDeclaration(declaration)
|
|
395
|
-
? projectionWithServerState({
|
|
396
|
-
declaration,
|
|
397
|
-
state: this.state,
|
|
398
|
-
previewBasePath: options.previewBasePath,
|
|
399
|
-
})
|
|
400
|
-
: declaration.projection,
|
|
401
|
-
},
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
if (this.state.status !== "starting" && this.state.status !== "ready") {
|
|
405
|
-
return {
|
|
406
|
-
disposition: { kind: "not_running" },
|
|
407
|
-
result: {
|
|
408
|
-
artifact: projectionWithServerState({
|
|
409
|
-
declaration,
|
|
410
|
-
state: this.state,
|
|
411
|
-
previewBasePath: options.previewBasePath,
|
|
412
|
-
}),
|
|
413
|
-
},
|
|
414
|
-
};
|
|
287
|
+
initializeWorkspace(options) {
|
|
288
|
+
if (this.declaration !== undefined || this.state !== undefined) {
|
|
289
|
+
return;
|
|
415
290
|
}
|
|
416
|
-
this.
|
|
417
|
-
return {
|
|
418
|
-
disposition: { kind: "recorded" },
|
|
419
|
-
result: {
|
|
420
|
-
artifact: projectionWithServerState({
|
|
421
|
-
declaration,
|
|
422
|
-
state: this.state,
|
|
423
|
-
previewBasePath: options.previewBasePath,
|
|
424
|
-
}),
|
|
425
|
-
},
|
|
426
|
-
};
|
|
291
|
+
this.refreshDeclaration(options.workspaceRoot, false, false);
|
|
427
292
|
}
|
|
428
|
-
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
if (
|
|
435
|
-
this.
|
|
436
|
-
return {
|
|
437
|
-
disposition: { kind: "not_declared" },
|
|
438
|
-
result: { artifact: declaration.projection },
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
if (declaration.status === "invalid") {
|
|
442
|
-
this.stopCurrentProcess();
|
|
443
|
-
return {
|
|
444
|
-
disposition: declarationInvalidDisposition(declaration).stop,
|
|
445
|
-
result: { artifact: declaration.projection },
|
|
446
|
-
};
|
|
293
|
+
renewLease(options) {
|
|
294
|
+
const nowMs = this.clockMs();
|
|
295
|
+
this.pruneExpiredLeases(nowMs);
|
|
296
|
+
const hadActiveLease = this.leases.size > 0;
|
|
297
|
+
this.leases.set(options.viewerId, nowMs);
|
|
298
|
+
this.scheduleLeaseSweep(nowMs);
|
|
299
|
+
if (!hadActiveLease) {
|
|
300
|
+
this.ensurePreview(options.workspaceRoot);
|
|
447
301
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
result: { artifact: declaration.projection },
|
|
453
|
-
};
|
|
302
|
+
}
|
|
303
|
+
beginWorkspaceUpdate() {
|
|
304
|
+
if (this.workspaceUpdating) {
|
|
305
|
+
return;
|
|
454
306
|
}
|
|
455
|
-
|
|
307
|
+
this.workspaceUpdating = true;
|
|
456
308
|
this.stopCurrentProcess();
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
declaration,
|
|
462
|
-
state: this.state,
|
|
463
|
-
previewBasePath: options.previewBasePath,
|
|
464
|
-
}),
|
|
465
|
-
},
|
|
466
|
-
};
|
|
309
|
+
}
|
|
310
|
+
settleWorkspaceUpdate(options) {
|
|
311
|
+
this.workspaceUpdating = false;
|
|
312
|
+
this.refreshDeclaration(options.workspaceRoot, this.leases.size > 0);
|
|
467
313
|
}
|
|
468
314
|
async fetchServerPreview(options) {
|
|
469
315
|
if (options.body !== undefined && options.body.byteLength > this.requestBodyLimitBytes) {
|
|
@@ -472,13 +318,8 @@ export class ArtifactPreviewManager {
|
|
|
472
318
|
if ((options.method === "GET" || options.method === "HEAD") && options.body !== undefined) {
|
|
473
319
|
throw new WorkspaceOpsError("bad_request", "Artifact preview request method cannot have a body");
|
|
474
320
|
}
|
|
475
|
-
const declaration = readArtifactDeclaration({
|
|
476
|
-
workspaceRoot: options.workspaceRoot,
|
|
477
|
-
now: this.now,
|
|
478
|
-
});
|
|
479
|
-
this.stopStaleRuntime(declaration);
|
|
480
321
|
const state = this.state;
|
|
481
|
-
if (
|
|
322
|
+
if (state?.declaration.runtime.kind !== "server") {
|
|
482
323
|
throw new WorkspaceOpsError("not_found", "Artifact preview is not running");
|
|
483
324
|
}
|
|
484
325
|
if (state.status !== "ready" || state.port === undefined || !processIsAlive(state.child)) {
|
|
@@ -490,16 +331,11 @@ export class ArtifactPreviewManager {
|
|
|
490
331
|
const requestPath = normalizePreviewRequestPath(options.path);
|
|
491
332
|
const localUrl = new URL(`http://127.0.0.1:${state.port}${requestPath}`);
|
|
492
333
|
const query = new URLSearchParams(options.query?.replace(/^\?/u, "") ?? "");
|
|
493
|
-
const
|
|
494
|
-
if (
|
|
495
|
-
throw new WorkspaceOpsError("not_found", "Artifact preview
|
|
496
|
-
}
|
|
497
|
-
const requestedInstanceId = query.get("preview");
|
|
498
|
-
if (requestedInstanceId !== null && requestedInstanceId !== state.instanceId) {
|
|
499
|
-
throw new WorkspaceOpsError("not_found", "Artifact preview instance is stale");
|
|
334
|
+
const requestedGeneration = query.get("g");
|
|
335
|
+
if (requestedGeneration !== state.generation) {
|
|
336
|
+
throw new WorkspaceOpsError("not_found", "Artifact preview generation is stale");
|
|
500
337
|
}
|
|
501
|
-
query.delete("
|
|
502
|
-
query.delete("preview");
|
|
338
|
+
query.delete("g");
|
|
503
339
|
localUrl.search = query.toString();
|
|
504
340
|
const requestHeaders = sanitizeRequestHeaders(options.headers);
|
|
505
341
|
if (options.headers?.origin !== undefined) {
|
|
@@ -551,41 +387,142 @@ export class ArtifactPreviewManager {
|
|
|
551
387
|
this.clearTimeoutFn(timeout);
|
|
552
388
|
}
|
|
553
389
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
390
|
+
readStaticPreview(options) {
|
|
391
|
+
const state = this.state;
|
|
392
|
+
if (state?.declaration.runtime.kind !== "static" ||
|
|
393
|
+
state.status !== "ready" ||
|
|
394
|
+
this.workspaceUpdating) {
|
|
395
|
+
throw new WorkspaceOpsError("not_found", "Artifact preview is not ready");
|
|
557
396
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
397
|
+
const query = new URLSearchParams(options.query?.replace(/^\?/u, "") ?? "");
|
|
398
|
+
const requestedGeneration = query.get("g");
|
|
399
|
+
if (requestedGeneration !== state.generation) {
|
|
400
|
+
throw new WorkspaceOpsError("not_found", "Artifact preview generation is stale");
|
|
561
401
|
}
|
|
562
|
-
|
|
402
|
+
return readArtifactStaticPreview({
|
|
403
|
+
workspaceRoot: options.workspaceRoot,
|
|
404
|
+
path: options.path,
|
|
405
|
+
declaration: state.declaration,
|
|
406
|
+
});
|
|
563
407
|
}
|
|
564
408
|
async stopAll() {
|
|
409
|
+
this.leases.clear();
|
|
410
|
+
if (this.leaseSweepTimer !== undefined) {
|
|
411
|
+
this.clearTimeoutFn(this.leaseSweepTimer);
|
|
412
|
+
this.leaseSweepTimer = undefined;
|
|
413
|
+
}
|
|
565
414
|
this.stopCurrentProcess();
|
|
566
415
|
while (this.pendingTasks.size > 0) {
|
|
567
416
|
await Promise.all([...this.pendingTasks]);
|
|
568
417
|
}
|
|
569
418
|
}
|
|
419
|
+
currentProjection(previewBasePath) {
|
|
420
|
+
const state = this.state;
|
|
421
|
+
return state === undefined
|
|
422
|
+
? this.lastProjection
|
|
423
|
+
: projectionWithState({
|
|
424
|
+
declaration: state.declaration,
|
|
425
|
+
state,
|
|
426
|
+
...(previewBasePath === undefined ? {} : { previewBasePath }),
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
ensurePreview(workspaceRoot) {
|
|
430
|
+
const current = this.state;
|
|
431
|
+
if (this.workspaceUpdating && current !== undefined) {
|
|
432
|
+
return current.declaration;
|
|
433
|
+
}
|
|
434
|
+
if (current !== undefined &&
|
|
435
|
+
(current.status === "starting" ||
|
|
436
|
+
current.status === "ready" ||
|
|
437
|
+
current.status === "failed")) {
|
|
438
|
+
return current.declaration;
|
|
439
|
+
}
|
|
440
|
+
const declaration = current?.declaration ?? this.declaration;
|
|
441
|
+
if (declaration !== undefined) {
|
|
442
|
+
return this.applyDeclaration(workspaceRoot, declaration, true);
|
|
443
|
+
}
|
|
444
|
+
return this.refreshDeclaration(workspaceRoot, true);
|
|
445
|
+
}
|
|
446
|
+
refreshDeclaration(workspaceRoot, activate, notify = true) {
|
|
447
|
+
const declaration = readLiveArtifactDeclaration({ workspaceRoot });
|
|
448
|
+
this.declaration = declaration;
|
|
449
|
+
return this.applyDeclaration(workspaceRoot, declaration, activate, notify);
|
|
450
|
+
}
|
|
451
|
+
applyDeclaration(workspaceRoot, declaration, activate, notify = true) {
|
|
452
|
+
if (this.state !== undefined) {
|
|
453
|
+
this.stopCurrentProcess();
|
|
454
|
+
this.state = undefined;
|
|
455
|
+
}
|
|
456
|
+
if (declaration.status !== "declared") {
|
|
457
|
+
this.lastProjection = declaration.projection;
|
|
458
|
+
if (notify) {
|
|
459
|
+
this.onStatusChange();
|
|
460
|
+
}
|
|
461
|
+
return declaration;
|
|
462
|
+
}
|
|
463
|
+
if (!activate) {
|
|
464
|
+
const state = this.createIdleState(declaration);
|
|
465
|
+
this.state = state;
|
|
466
|
+
this.lastProjection = projectionWithState({ declaration, state });
|
|
467
|
+
if (notify) {
|
|
468
|
+
this.onStatusChange();
|
|
469
|
+
}
|
|
470
|
+
return declaration;
|
|
471
|
+
}
|
|
472
|
+
if (declaration.runtime.kind === "server") {
|
|
473
|
+
this.startProcess(workspaceRoot, declaration);
|
|
474
|
+
return declaration;
|
|
475
|
+
}
|
|
476
|
+
const generation = this.nextGeneration();
|
|
477
|
+
const state = {
|
|
478
|
+
generation,
|
|
479
|
+
declaration,
|
|
480
|
+
entryPath: declaration.runtime.entry_path,
|
|
481
|
+
cwdPath: "",
|
|
482
|
+
status: "ready",
|
|
483
|
+
child: undefined,
|
|
484
|
+
processRuntime: undefined,
|
|
485
|
+
port: undefined,
|
|
486
|
+
launchAttempt: 0,
|
|
487
|
+
errorMessage: undefined,
|
|
488
|
+
diagnostics: new ArtifactDiagnosticBuffer(),
|
|
489
|
+
};
|
|
490
|
+
this.state = state;
|
|
491
|
+
this.lastProjection = projectionWithState({ declaration, state });
|
|
492
|
+
this.onStatusChange();
|
|
493
|
+
return declaration;
|
|
494
|
+
}
|
|
495
|
+
createIdleState(declaration) {
|
|
496
|
+
return {
|
|
497
|
+
generation: this.nextGeneration(),
|
|
498
|
+
declaration,
|
|
499
|
+
entryPath: declaration.runtime.entry_path,
|
|
500
|
+
cwdPath: "",
|
|
501
|
+
status: "idle",
|
|
502
|
+
child: undefined,
|
|
503
|
+
processRuntime: undefined,
|
|
504
|
+
port: undefined,
|
|
505
|
+
launchAttempt: 0,
|
|
506
|
+
errorMessage: undefined,
|
|
507
|
+
diagnostics: new ArtifactDiagnosticBuffer(),
|
|
508
|
+
};
|
|
509
|
+
}
|
|
570
510
|
startProcess(workspaceRoot, declaration) {
|
|
511
|
+
const generation = this.nextGeneration();
|
|
571
512
|
const state = {
|
|
572
|
-
|
|
573
|
-
|
|
513
|
+
generation,
|
|
514
|
+
declaration,
|
|
574
515
|
entryPath: declaration.runtime.entry_path,
|
|
575
516
|
cwdPath: resolveServerCwd(workspaceRoot, declaration.runtime.cwd_path),
|
|
576
517
|
status: "starting",
|
|
577
518
|
child: undefined,
|
|
578
519
|
processRuntime: undefined,
|
|
579
520
|
port: undefined,
|
|
580
|
-
instanceId: this.nextInstanceId(),
|
|
581
521
|
launchAttempt: 0,
|
|
582
|
-
lastHeartbeatAtMs: this.clockMs(),
|
|
583
522
|
errorMessage: undefined,
|
|
584
523
|
diagnostics: new ArtifactDiagnosticBuffer(),
|
|
585
|
-
idleTimer: undefined,
|
|
586
524
|
};
|
|
587
525
|
this.state = state;
|
|
588
|
-
this.scheduleIdleSweep(state);
|
|
589
526
|
this.onStatusChange();
|
|
590
527
|
this.trackTask(this.launchProcess(state));
|
|
591
528
|
}
|
|
@@ -633,7 +570,7 @@ export class ArtifactPreviewManager {
|
|
|
633
570
|
}
|
|
634
571
|
});
|
|
635
572
|
child.on("close", (code, signal) => {
|
|
636
|
-
if (this.state !== state || state.child !== child || state.status === "
|
|
573
|
+
if (this.state !== state || state.child !== child || state.status === "idle") {
|
|
637
574
|
this.cleanupStateRuntime(state, runtime);
|
|
638
575
|
return;
|
|
639
576
|
}
|
|
@@ -746,50 +683,55 @@ export class ArtifactPreviewManager {
|
|
|
746
683
|
}
|
|
747
684
|
return { kind: "invalid", message: "Preview entry redirected too many times" };
|
|
748
685
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
if (state.idleTimer !== undefined) {
|
|
755
|
-
this.clearTimeoutFn(state.idleTimer);
|
|
686
|
+
pruneExpiredLeases(nowMs) {
|
|
687
|
+
for (const [viewerId, renewedAtMs] of this.leases) {
|
|
688
|
+
if (nowMs - renewedAtMs >= this.idleStopTtlMs) {
|
|
689
|
+
this.leases.delete(viewerId);
|
|
690
|
+
}
|
|
756
691
|
}
|
|
757
|
-
state.idleTimer = this.setTimeoutFn(() => this.sweepIdle(), this.idleStopTtlMs);
|
|
758
|
-
unrefTimer(state.idleTimer);
|
|
759
692
|
}
|
|
760
|
-
|
|
761
|
-
if (this.
|
|
762
|
-
|
|
693
|
+
scheduleLeaseSweep(nowMs = this.clockMs()) {
|
|
694
|
+
if (this.leaseSweepTimer !== undefined) {
|
|
695
|
+
this.clearTimeoutFn(this.leaseSweepTimer);
|
|
696
|
+
this.leaseSweepTimer = undefined;
|
|
763
697
|
}
|
|
764
|
-
if (
|
|
765
|
-
|
|
766
|
-
this.state = undefined;
|
|
698
|
+
if (this.leases.size === 0) {
|
|
699
|
+
return;
|
|
767
700
|
}
|
|
701
|
+
let earliestExpiryMs = Number.POSITIVE_INFINITY;
|
|
702
|
+
for (const renewedAtMs of this.leases.values()) {
|
|
703
|
+
earliestExpiryMs = Math.min(earliestExpiryMs, renewedAtMs + this.idleStopTtlMs);
|
|
704
|
+
}
|
|
705
|
+
this.leaseSweepTimer = this.setTimeoutFn(() => {
|
|
706
|
+
this.leaseSweepTimer = undefined;
|
|
707
|
+
const sweepAtMs = this.clockMs();
|
|
708
|
+
this.pruneExpiredLeases(sweepAtMs);
|
|
709
|
+
if (this.leases.size === 0) {
|
|
710
|
+
if (this.state?.status !== "failed") {
|
|
711
|
+
this.stopCurrentProcess();
|
|
712
|
+
}
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
this.scheduleLeaseSweep(sweepAtMs);
|
|
716
|
+
}, Math.max(0, earliestExpiryMs - nowMs));
|
|
717
|
+
unrefTimer(this.leaseSweepTimer);
|
|
768
718
|
}
|
|
769
719
|
stopCurrentProcess() {
|
|
770
720
|
const state = this.state;
|
|
771
|
-
if (state === undefined) {
|
|
721
|
+
if (state === undefined || state.status === "idle") {
|
|
772
722
|
return;
|
|
773
723
|
}
|
|
774
|
-
if (state.idleTimer !== undefined) {
|
|
775
|
-
this.clearTimeoutFn(state.idleTimer);
|
|
776
|
-
state.idleTimer = undefined;
|
|
777
|
-
}
|
|
778
724
|
this.disposeStateProcess(state);
|
|
779
|
-
state.status = "
|
|
725
|
+
state.status = "idle";
|
|
780
726
|
state.errorMessage = undefined;
|
|
781
727
|
this.onStatusChange();
|
|
782
728
|
}
|
|
783
729
|
failState(state, message) {
|
|
784
|
-
if (this.state !== state || state.status === "
|
|
730
|
+
if (this.state !== state || state.status === "idle") {
|
|
785
731
|
return;
|
|
786
732
|
}
|
|
787
733
|
state.status = "failed";
|
|
788
734
|
state.errorMessage = redactAndTruncateText(message, 220);
|
|
789
|
-
if (state.idleTimer !== undefined) {
|
|
790
|
-
this.clearTimeoutFn(state.idleTimer);
|
|
791
|
-
state.idleTimer = undefined;
|
|
792
|
-
}
|
|
793
735
|
this.disposeStateProcess(state);
|
|
794
736
|
this.onStatusChange();
|
|
795
737
|
}
|
|
@@ -822,9 +764,9 @@ export class ArtifactPreviewManager {
|
|
|
822
764
|
this.pendingTasks.add(task);
|
|
823
765
|
void task.finally(() => this.pendingTasks.delete(task));
|
|
824
766
|
}
|
|
825
|
-
|
|
826
|
-
this.
|
|
827
|
-
return `artifact_preview_${this.
|
|
767
|
+
nextGeneration() {
|
|
768
|
+
this.nextGenerationSequence += 1;
|
|
769
|
+
return `artifact_preview_${this.nextGenerationSequence}`;
|
|
828
770
|
}
|
|
829
771
|
}
|
|
830
772
|
export const ARTIFACT_PREVIEW_DEFAULT_BASE_PATH = ARTIFACT_PREVIEW_BASE_PATH;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type LiveArtifactDeclaration } from "./live-manifest.js";
|
|
1
2
|
export declare const ARTIFACT_PREVIEW_CSP: string;
|
|
2
3
|
export type ArtifactStaticPreviewFile = {
|
|
3
4
|
media_type: string;
|
|
@@ -6,7 +7,7 @@ export type ArtifactStaticPreviewFile = {
|
|
|
6
7
|
export type ReadArtifactStaticPreviewOptions = {
|
|
7
8
|
workspaceRoot: string;
|
|
8
9
|
path: string | undefined;
|
|
9
|
-
|
|
10
|
+
declaration?: LiveArtifactDeclaration;
|
|
10
11
|
};
|
|
11
12
|
export declare function rewriteArtifactPreviewResourceUrls(options: {
|
|
12
13
|
mediaType: string | undefined;
|