@milaboratories/pl-middle-layer 1.66.4 → 1.66.6

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.
@@ -9,10 +9,19 @@ import { BLOCK_STORAGE_FACADE_VERSION, UiError, extractConfig } from "@platforma
9
9
  import { InitialBlockSettings } from "@milaboratories/pl-model-middle-layer";
10
10
  import { ConsoleLoggerAdapter, cachedDecode, cachedDeserialize, canonicalJsonBytes, notEmpty } from "@milaboratories/ts-helpers";
11
11
  import { gzipSync } from "node:zlib";
12
- import { Pl, PlClient, ensureSignedResourceIdNotNull, field, isNotNullSignedResourceId, isNullSignedResourceId, isResource, isResourceId, isResourceRef } from "@milaboratories/pl-client";
12
+ import { Pl, PlClient, ensureSignedResourceIdNotNull, field, isNotNullSignedResourceId, isNullSignedResourceId, isResource, isResourceId, isResourceRef, readyOrDuplicateOrError } from "@milaboratories/pl-client";
13
13
  import { ModelAPIVersionMismatchError } from "@milaboratories/pl-errors";
14
14
  import Denque from "denque";
15
15
  //#region src/mutator/project.ts
16
+ /**
17
+ * A production core field is "settled" once its result is immutable — `Ready` or a finished
18
+ * `Error`. `NotReady` (still computing) is the only unsettled status; a missing field is unsettled.
19
+ * Settled production is safe to keep/share; only running production is dropped. The resource-level
20
+ * equivalent is {@link readyOrDuplicateOrError}.
21
+ */
22
+ function isProductionFieldSettled(status) {
23
+ return status === "Ready" || status === "Error";
24
+ }
16
25
  function cached(modIdCb, valueCb) {
17
26
  let initialized = false;
18
27
  let lastModId = void 0;
@@ -240,6 +249,32 @@ var ProjectMutator = class ProjectMutator {
240
249
  if (this.actualProductionGraph === void 0) this.actualProductionGraph = productionGraph(this.struct, (blockId) => this.getProductionGraphBlockInfo(blockId, true));
241
250
  return this.actualProductionGraph;
242
251
  }
252
+ /**
253
+ * Blocks whose production must NOT be shared with a copy of the project: those still computing
254
+ * ("NotReady"), plus their downstream closure. Settled production — `Ready` or a finished
255
+ * `Error` — is kept: it is immutable, so sharing by reference is safe, and sharing an errored
256
+ * project (to show it failed) is a valid use-case. Only running production would tie the copy to
257
+ * the source's live computation.
258
+ *
259
+ * The closure keeps the chain consistent: a finished block's `prodCtx` is built from its
260
+ * upstreams' `prodCtx` (see {@link createProdCtx}), so keeping it while dropping a running
261
+ * upstream would leave it referencing an absent context — a break {@link check} misses. Settled
262
+ * blocks are never seeds, so their downstream is retained.
263
+ *
264
+ * Blocks without production are ignored. Returns empty when nothing is running.
265
+ */
266
+ productionBlocksToDropForCopy() {
267
+ const dropSet = /* @__PURE__ */ new Set();
268
+ for (const [blockId, info] of this.blockInfos) {
269
+ if (!(info.fields.prodOutput !== void 0 || info.fields.prodCtx !== void 0)) continue;
270
+ if (!isProductionFieldSettled(info.fields.prodOutput?.status) || !isProductionFieldSettled(info.fields.prodCtx?.status)) dropSet.add(blockId);
271
+ }
272
+ if (dropSet.size === 0) return dropSet;
273
+ const graph = this.getActualProductionGraph();
274
+ const seeds = [...dropSet].filter((id) => graph.nodes.has(id));
275
+ for (const id of graph.traverseIds("downstream", ...seeds)) dropSet.add(id);
276
+ return dropSet;
277
+ }
243
278
  getBlockInfo(blockId) {
244
279
  const info = this.blockInfos.get(blockId);
245
280
  if (info === void 0) throw new Error(`No such block: ${blockId}`);
@@ -354,12 +389,12 @@ var ProjectMutator = class ProjectMutator {
354
389
  }
355
390
  this.deleteBlockFields(blockId, "prodOutput", "prodCtx", "prodUiCtx", "prodArgs");
356
391
  }
357
- /** Running blocks are reset, already computed moved to limbo. Returns if
392
+ /** Running blocks are reset, settled ones (Ready or finished Error) moved to limbo. Returns if
358
393
  * either of the actions were actually performed.
359
394
  * This method ensures the block is left in a consistent state that passes check() constraints. */
360
395
  resetOrLimboProduction(blockId) {
361
396
  const fields = this.getBlockInfo(blockId).fields;
362
- if (fields.prodOutput?.status === "Ready" && fields.prodCtx?.status === "Ready") {
397
+ if (isProductionFieldSettled(fields.prodOutput?.status) && isProductionFieldSettled(fields.prodCtx?.status)) {
363
398
  if (this.blocksInLimbo.has(blockId)) return false;
364
399
  this.blocksInLimbo.add(blockId);
365
400
  this.renderingStateChanged = true;
@@ -592,7 +627,7 @@ var ProjectMutator = class ProjectMutator {
592
627
  getFieldNamesToDuplicate(blockId) {
593
628
  const fields = this.getBlockInfo(blockId).fields;
594
629
  const diff = (setA, setB) => new Set([...setA].filter((x) => !setB.has(x)));
595
- if (fields.prodOutput?.status === "Ready" && fields.prodCtx?.status === "Ready") {
630
+ if (isProductionFieldSettled(fields.prodOutput?.status) && isProductionFieldSettled(fields.prodCtx?.status)) {
596
631
  if (this.blocksInLimbo.has(blockId)) return FieldsToDuplicate;
597
632
  return diff(FieldsToDuplicate, /* @__PURE__ */ new Set([
598
633
  "prodOutputPrevious",
@@ -1007,7 +1042,7 @@ async function createProject(tx, meta = InitialBlockMeta) {
1007
1042
  * @param sourceRid - resource id of the project to duplicate
1008
1043
  * @param options.label - optional label override for the new project; if omitted, copies the source label
1009
1044
  */
1010
- async function duplicateProject(tx, sourceRid, options) {
1045
+ async function duplicateProject(tx, sourceRid, options, projectHelper) {
1011
1046
  const sourceDataP = tx.getResourceData(sourceRid, true);
1012
1047
  const sourceKVsP = tx.listKeyValuesString(sourceRid);
1013
1048
  const sourceData = await sourceDataP;
@@ -1017,6 +1052,36 @@ async function duplicateProject(tx, sourceRid, options) {
1017
1052
  if (schema !== "4" && schema !== "3") throw new UiError(`Cannot duplicate project with schema version ${schema ?? "unknown"}. Only schema versions 3 and 4 are supported. Try opening the project first to trigger migration.`);
1018
1053
  const newPrj = tx.createEphemeral(ProjectResourceType);
1019
1054
  tx.lock(newPrj);
1055
+ const prodFields = /* @__PURE__ */ new Set([
1056
+ "prodOutput",
1057
+ "prodCtx",
1058
+ "prodUiCtx",
1059
+ "prodArgs"
1060
+ ]);
1061
+ const isTargetSettled = async (rid) => {
1062
+ if (rid === void 0 || isNullSignedResourceId(rid)) return false;
1063
+ try {
1064
+ return readyOrDuplicateOrError(await tx.getResourceData(rid, false));
1065
+ } catch {
1066
+ return false;
1067
+ }
1068
+ };
1069
+ const prodCoreByBlock = /* @__PURE__ */ new Map();
1070
+ for (const f of sourceData.fields) {
1071
+ if (isNullSignedResourceId(f.value)) continue;
1072
+ const parsed = parseProjectField(f.name);
1073
+ if (parsed === void 0) continue;
1074
+ if (parsed.fieldName === "prodOutput" || parsed.fieldName === "prodCtx") {
1075
+ const core = prodCoreByBlock.get(parsed.blockId) ?? {};
1076
+ core[parsed.fieldName] = f.value;
1077
+ prodCoreByBlock.set(parsed.blockId, core);
1078
+ }
1079
+ }
1080
+ const anyRunning = (await Promise.all([...prodCoreByBlock.values()].map(async (core) => !(await isTargetSettled(core.prodOutput) && await isTargetSettled(core.prodCtx))))).some((running) => running);
1081
+ let dropProduction;
1082
+ if (!anyRunning) dropProduction = /* @__PURE__ */ new Set();
1083
+ else if (projectHelper !== void 0 && schema === "4") dropProduction = (await ProjectMutator.load(projectHelper, tx, sourceRid)).productionBlocksToDropForCopy();
1084
+ else dropProduction = new Set(prodCoreByBlock.keys());
1020
1085
  const ts = String(Date.now());
1021
1086
  const kvSkipPrefixes = [BlockArgsAuthorKeyPrefix];
1022
1087
  const kvSkipKeys = /* @__PURE__ */ new Set([
@@ -1041,6 +1106,7 @@ async function duplicateProject(tx, sourceRid, options) {
1041
1106
  if (isNullSignedResourceId(f.value)) continue;
1042
1107
  const parsed = parseProjectField(f.name);
1043
1108
  if (parsed !== void 0 && !FieldsToDuplicate.has(parsed.fieldName)) continue;
1109
+ if (parsed !== void 0 && prodFields.has(parsed.fieldName) && dropProduction.has(parsed.blockId)) continue;
1044
1110
  tx.createField(field(newPrj, f.name), "Dynamic", f.value);
1045
1111
  }
1046
1112
  return newPrj;