@milaboratories/pl-middle-layer 1.66.4 → 1.66.5
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/middle_layer/middle_layer.cjs +2 -2
- package/dist/middle_layer/middle_layer.cjs.map +1 -1
- package/dist/middle_layer/middle_layer.d.ts.map +1 -1
- package/dist/middle_layer/middle_layer.js +2 -2
- package/dist/middle_layer/middle_layer.js.map +1 -1
- package/dist/mutator/project.cjs +70 -4
- package/dist/mutator/project.cjs.map +1 -1
- package/dist/mutator/project.d.ts +9 -1
- package/dist/mutator/project.d.ts.map +1 -1
- package/dist/mutator/project.js +71 -5
- package/dist/mutator/project.js.map +1 -1
- package/package.json +9 -9
- package/src/middle_layer/middle_layer.ts +12 -2
- package/src/mutator/project.ts +148 -7
package/src/mutator/project.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
isResourceRef,
|
|
19
19
|
Pl,
|
|
20
20
|
PlClient,
|
|
21
|
+
readyOrDuplicateOrError,
|
|
21
22
|
} from "@milaboratories/pl-client";
|
|
22
23
|
import {
|
|
23
24
|
createRenderHeavyBlock,
|
|
@@ -87,6 +88,16 @@ import type { BlockPackInfo } from "../model/block_pack";
|
|
|
87
88
|
|
|
88
89
|
type FieldStatus = "NotReady" | "Ready" | "Error";
|
|
89
90
|
|
|
91
|
+
/**
|
|
92
|
+
* A production core field is "settled" once its result is immutable — `Ready` or a finished
|
|
93
|
+
* `Error`. `NotReady` (still computing) is the only unsettled status; a missing field is unsettled.
|
|
94
|
+
* Settled production is safe to keep/share; only running production is dropped. The resource-level
|
|
95
|
+
* equivalent is {@link readyOrDuplicateOrError}.
|
|
96
|
+
*/
|
|
97
|
+
function isProductionFieldSettled(status: FieldStatus | undefined): boolean {
|
|
98
|
+
return status === "Ready" || status === "Error";
|
|
99
|
+
}
|
|
100
|
+
|
|
90
101
|
interface BlockFieldState {
|
|
91
102
|
modCount: number;
|
|
92
103
|
ref?: AnyRef;
|
|
@@ -521,6 +532,43 @@ export class ProjectMutator {
|
|
|
521
532
|
return this.actualProductionGraph;
|
|
522
533
|
}
|
|
523
534
|
|
|
535
|
+
/**
|
|
536
|
+
* Blocks whose production must NOT be shared with a copy of the project: those still computing
|
|
537
|
+
* ("NotReady"), plus their downstream closure. Settled production — `Ready` or a finished
|
|
538
|
+
* `Error` — is kept: it is immutable, so sharing by reference is safe, and sharing an errored
|
|
539
|
+
* project (to show it failed) is a valid use-case. Only running production would tie the copy to
|
|
540
|
+
* the source's live computation.
|
|
541
|
+
*
|
|
542
|
+
* The closure keeps the chain consistent: a finished block's `prodCtx` is built from its
|
|
543
|
+
* upstreams' `prodCtx` (see {@link createProdCtx}), so keeping it while dropping a running
|
|
544
|
+
* upstream would leave it referencing an absent context — a break {@link check} misses. Settled
|
|
545
|
+
* blocks are never seeds, so their downstream is retained.
|
|
546
|
+
*
|
|
547
|
+
* Blocks without production are ignored. Returns empty when nothing is running.
|
|
548
|
+
*/
|
|
549
|
+
public productionBlocksToDropForCopy(): Set<string> {
|
|
550
|
+
const dropSet = new Set<string>();
|
|
551
|
+
for (const [blockId, info] of this.blockInfos) {
|
|
552
|
+
const hasProduction =
|
|
553
|
+
info.fields.prodOutput !== undefined || info.fields.prodCtx !== undefined;
|
|
554
|
+
if (!hasProduction) continue;
|
|
555
|
+
// Drop only running production; keep both settled states (Ready and Error).
|
|
556
|
+
const running =
|
|
557
|
+
!isProductionFieldSettled(info.fields.prodOutput?.status) ||
|
|
558
|
+
!isProductionFieldSettled(info.fields.prodCtx?.status);
|
|
559
|
+
if (running) dropSet.add(blockId);
|
|
560
|
+
}
|
|
561
|
+
if (dropSet.size === 0) return dropSet;
|
|
562
|
+
|
|
563
|
+
// Snapshot before expanding, so the closure can't re-seed from its own additions. Seed only
|
|
564
|
+
// graph-present blocks: a partial block may lack prodArgs and not be a graph node, and
|
|
565
|
+
// traverse() throws on a missing root.
|
|
566
|
+
const graph = this.getActualProductionGraph();
|
|
567
|
+
const seeds = [...dropSet].filter((id) => graph.nodes.has(id));
|
|
568
|
+
for (const id of graph.traverseIds("downstream", ...seeds)) dropSet.add(id);
|
|
569
|
+
return dropSet;
|
|
570
|
+
}
|
|
571
|
+
|
|
524
572
|
//
|
|
525
573
|
// Generic helpers to interact with project state
|
|
526
574
|
//
|
|
@@ -661,19 +709,23 @@ export class ProjectMutator {
|
|
|
661
709
|
this.deleteBlockFields(blockId, "prodOutput", "prodCtx", "prodUiCtx", "prodArgs");
|
|
662
710
|
}
|
|
663
711
|
|
|
664
|
-
/** Running blocks are reset,
|
|
712
|
+
/** Running blocks are reset, settled ones (Ready or finished Error) moved to limbo. Returns if
|
|
665
713
|
* either of the actions were actually performed.
|
|
666
714
|
* This method ensures the block is left in a consistent state that passes check() constraints. */
|
|
667
715
|
private resetOrLimboProduction(blockId: string): boolean {
|
|
668
716
|
const fields = this.getBlockInfo(blockId).fields;
|
|
669
717
|
|
|
670
|
-
//
|
|
671
|
-
|
|
718
|
+
// Move to limbo only if both core production fields have settled (a finished Error is kept, not
|
|
719
|
+
// reset — sharing/keeping a failed block is valid); otherwise reset.
|
|
720
|
+
if (
|
|
721
|
+
isProductionFieldSettled(fields.prodOutput?.status) &&
|
|
722
|
+
isProductionFieldSettled(fields.prodCtx?.status)
|
|
723
|
+
) {
|
|
672
724
|
if (this.blocksInLimbo.has(blockId))
|
|
673
725
|
// we are already in limbo
|
|
674
726
|
return false;
|
|
675
727
|
|
|
676
|
-
// limbo - keep the
|
|
728
|
+
// limbo - keep the settled production results but clean up cache
|
|
677
729
|
this.blocksInLimbo.add(blockId);
|
|
678
730
|
this.renderingStateChanged = true;
|
|
679
731
|
|
|
@@ -1164,8 +1216,11 @@ export class ProjectMutator {
|
|
|
1164
1216
|
const diff = <T>(setA: Set<T>, setB: Set<T>): Set<T> =>
|
|
1165
1217
|
new Set([...setA].filter((x) => !setB.has(x)));
|
|
1166
1218
|
|
|
1167
|
-
//
|
|
1168
|
-
if (
|
|
1219
|
+
// Keep settled production (Ready or finished Error); drop it only while still running.
|
|
1220
|
+
if (
|
|
1221
|
+
isProductionFieldSettled(fields.prodOutput?.status) &&
|
|
1222
|
+
isProductionFieldSettled(fields.prodCtx?.status)
|
|
1223
|
+
) {
|
|
1169
1224
|
if (this.blocksInLimbo.has(blockId))
|
|
1170
1225
|
// we are already in limbo
|
|
1171
1226
|
return FieldsToDuplicate;
|
|
@@ -1957,6 +2012,13 @@ export async function duplicateProject(
|
|
|
1957
2012
|
tx: PlTransaction,
|
|
1958
2013
|
sourceRid: SignedResourceId,
|
|
1959
2014
|
options?: { label?: string },
|
|
2015
|
+
/**
|
|
2016
|
+
* Optional model access, used only to compute the precise drop set when the source has running
|
|
2017
|
+
* production (see below). Omitted, or a non-current schema → conservative all-or-nothing drop
|
|
2018
|
+
* (still correct, less selective). Interactive callers pass it for precision; callers without a
|
|
2019
|
+
* handy helper (sharing, CLI) omit it.
|
|
2020
|
+
*/
|
|
2021
|
+
projectHelper?: ProjectHelper,
|
|
1960
2022
|
): Promise<ResourceRef> {
|
|
1961
2023
|
// Read source resource data (with fields) and all KV pairs
|
|
1962
2024
|
const sourceDataP = tx.getResourceData(sourceRid, true);
|
|
@@ -1980,6 +2042,77 @@ export async function duplicateProject(
|
|
|
1980
2042
|
const newPrj = tx.createEphemeral(ProjectResourceType);
|
|
1981
2043
|
tx.lock(newPrj);
|
|
1982
2044
|
|
|
2045
|
+
// Production fields (prodOutput/prodCtx/prodUiCtx/prodArgs) are copied by reference — the copy
|
|
2046
|
+
// points at the SAME backend resources. Safe only for *settled* production (Ready or finished
|
|
2047
|
+
// Error, both immutable); a running (NotReady) block would tie the copy to the source's live
|
|
2048
|
+
// computation. Errored production is kept on purpose — sharing a failed project is a valid case.
|
|
2049
|
+
//
|
|
2050
|
+
// Drop running blocks plus their downstream closure (a finished block's prodCtx is built from its
|
|
2051
|
+
// upstreams' — see createProdCtx — so keeping it while dropping a running upstream breaks the
|
|
2052
|
+
// chain). The closure needs the model-layer dependency graph (ProjectHelper + current schema):
|
|
2053
|
+
// - fast path: cheap resource-level scan; nothing running → copy all production;
|
|
2054
|
+
// - precise path: something running, graph available → drop running blocks + downstream closure;
|
|
2055
|
+
// - fallback: something running, no graph (no helper / unmigrated schema) → drop ALL production.
|
|
2056
|
+
// Conservative, but always chain-consistent.
|
|
2057
|
+
const prodFields = new Set<ProjectField["fieldName"]>([
|
|
2058
|
+
"prodOutput",
|
|
2059
|
+
"prodCtx",
|
|
2060
|
+
"prodUiCtx",
|
|
2061
|
+
"prodArgs",
|
|
2062
|
+
]);
|
|
2063
|
+
|
|
2064
|
+
// Has this target settled (data will no longer change: Ready, deduplicated, or finished-error)?
|
|
2065
|
+
// Schema-agnostic, no model layer. Exactly the canonical readyOrDuplicateOrError predicate, which
|
|
2066
|
+
// matches the field-status rule in ProjectMutator.load (NotReady iff none of those hold).
|
|
2067
|
+
const isTargetSettled = async (rid: SignedResourceId | undefined): Promise<boolean> => {
|
|
2068
|
+
if (rid === undefined || isNullSignedResourceId(rid)) return false;
|
|
2069
|
+
try {
|
|
2070
|
+
const r = await tx.getResourceData(rid, false);
|
|
2071
|
+
return readyOrDuplicateOrError(r);
|
|
2072
|
+
} catch {
|
|
2073
|
+
// Gone/inaccessible → not settled: drop and re-derive in the copy rather than fail the whole
|
|
2074
|
+
// duplicate.
|
|
2075
|
+
return false;
|
|
2076
|
+
}
|
|
2077
|
+
};
|
|
2078
|
+
|
|
2079
|
+
// Collect each block's prodOutput/prodCtx targets (the blocks that have production).
|
|
2080
|
+
const prodCoreByBlock = new Map<
|
|
2081
|
+
string,
|
|
2082
|
+
{ prodOutput?: SignedResourceId; prodCtx?: SignedResourceId }
|
|
2083
|
+
>();
|
|
2084
|
+
for (const f of sourceData.fields) {
|
|
2085
|
+
if (isNullSignedResourceId(f.value)) continue;
|
|
2086
|
+
const parsed = parseProjectField(f.name);
|
|
2087
|
+
if (parsed === undefined) continue;
|
|
2088
|
+
if (parsed.fieldName === "prodOutput" || parsed.fieldName === "prodCtx") {
|
|
2089
|
+
const core = prodCoreByBlock.get(parsed.blockId) ?? {};
|
|
2090
|
+
core[parsed.fieldName] = f.value;
|
|
2091
|
+
prodCoreByBlock.set(parsed.blockId, core);
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
const anyRunning = (
|
|
2095
|
+
await Promise.all(
|
|
2096
|
+
[...prodCoreByBlock.values()].map(
|
|
2097
|
+
async (core) =>
|
|
2098
|
+
!((await isTargetSettled(core.prodOutput)) && (await isTargetSettled(core.prodCtx))),
|
|
2099
|
+
),
|
|
2100
|
+
)
|
|
2101
|
+
).some((running) => running);
|
|
2102
|
+
|
|
2103
|
+
let dropProduction: ReadonlySet<string>;
|
|
2104
|
+
if (!anyRunning) {
|
|
2105
|
+
dropProduction = new Set(); // fast path: all production settled (Ready/error) → copy as-is
|
|
2106
|
+
} else if (projectHelper !== undefined && schema === SchemaVersionCurrent) {
|
|
2107
|
+
// Precise path: drop running blocks + their downstream closure, computed from the source's
|
|
2108
|
+
// dependency graph. The copy shares the source's block ids, so the set applies directly.
|
|
2109
|
+
const srcMutator = await ProjectMutator.load(projectHelper, tx, sourceRid);
|
|
2110
|
+
dropProduction = srcMutator.productionBlocksToDropForCopy();
|
|
2111
|
+
} else {
|
|
2112
|
+
// Fallback: no graph available → drop all production (every block that has any).
|
|
2113
|
+
dropProduction = new Set(prodCoreByBlock.keys());
|
|
2114
|
+
}
|
|
2115
|
+
|
|
1983
2116
|
// Copy KV pairs with adjustments
|
|
1984
2117
|
const ts = String(Date.now());
|
|
1985
2118
|
const kvSkipPrefixes = [BlockArgsAuthorKeyPrefix];
|
|
@@ -1999,7 +2132,8 @@ export async function duplicateProject(
|
|
|
1999
2132
|
const meta: ProjectMeta = JSON.parse(value);
|
|
2000
2133
|
tx.setKValue(newPrj, key, JSON.stringify({ ...meta, label: options.label }));
|
|
2001
2134
|
} else {
|
|
2002
|
-
// Copy as-is
|
|
2135
|
+
// Copy as-is. Limbo entries are left untouched: a limbo block's production is Ready (kept, so
|
|
2136
|
+
// never dropped), and a stale entry self-heals when the copy is opened.
|
|
2003
2137
|
tx.setKValue(newPrj, key, value);
|
|
2004
2138
|
}
|
|
2005
2139
|
}
|
|
@@ -2017,6 +2151,13 @@ export async function duplicateProject(
|
|
|
2017
2151
|
if (isNullSignedResourceId(f.value)) continue;
|
|
2018
2152
|
const parsed = parseProjectField(f.name);
|
|
2019
2153
|
if (parsed !== undefined && !FieldsToDuplicate.has(parsed.fieldName)) continue;
|
|
2154
|
+
// Drop production for running blocks and their downstream closure (see above).
|
|
2155
|
+
if (
|
|
2156
|
+
parsed !== undefined &&
|
|
2157
|
+
prodFields.has(parsed.fieldName) &&
|
|
2158
|
+
dropProduction.has(parsed.blockId)
|
|
2159
|
+
)
|
|
2160
|
+
continue;
|
|
2020
2161
|
tx.createField(field(newPrj, f.name), "Dynamic", f.value);
|
|
2021
2162
|
}
|
|
2022
2163
|
|