@milaboratories/pl-middle-layer 1.51.0 → 1.52.1

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.
Files changed (61) hide show
  1. package/dist/cfg_render/executor.cjs +2 -2
  2. package/dist/cfg_render/executor.js +1 -1
  3. package/dist/debug/index.cjs +4 -1
  4. package/dist/debug/index.cjs.map +1 -1
  5. package/dist/debug/index.js +4 -1
  6. package/dist/debug/index.js.map +1 -1
  7. package/dist/index.cjs +14 -0
  8. package/dist/index.d.ts +3 -2
  9. package/dist/index.js +2 -1
  10. package/dist/js_render/computable_context.cjs +12 -2
  11. package/dist/js_render/computable_context.cjs.map +1 -1
  12. package/dist/js_render/computable_context.js +12 -2
  13. package/dist/js_render/computable_context.js.map +1 -1
  14. package/dist/js_render/context.cjs +36 -3
  15. package/dist/js_render/context.cjs.map +1 -1
  16. package/dist/js_render/context.js +36 -3
  17. package/dist/js_render/context.js.map +1 -1
  18. package/dist/js_render/index.cjs +5 -1
  19. package/dist/js_render/index.cjs.map +1 -1
  20. package/dist/js_render/index.js +5 -1
  21. package/dist/js_render/index.js.map +1 -1
  22. package/dist/middle_layer/project.cjs +8 -5
  23. package/dist/middle_layer/project.cjs.map +1 -1
  24. package/dist/middle_layer/project.js +8 -5
  25. package/dist/middle_layer/project.js.map +1 -1
  26. package/dist/middle_layer/project_overview.cjs +28 -22
  27. package/dist/middle_layer/project_overview.cjs.map +1 -1
  28. package/dist/middle_layer/project_overview.js +28 -22
  29. package/dist/middle_layer/project_overview.js.map +1 -1
  30. package/dist/model/block_pack_spec.cjs.map +1 -1
  31. package/dist/model/block_pack_spec.d.ts +2 -2
  32. package/dist/model/block_pack_spec.js.map +1 -1
  33. package/dist/model/template_spec.d.ts +7 -2
  34. package/dist/mutator/block-pack/block_pack.cjs +20 -1
  35. package/dist/mutator/block-pack/block_pack.cjs.map +1 -1
  36. package/dist/mutator/block-pack/block_pack.d.ts +4 -0
  37. package/dist/mutator/block-pack/block_pack.js +19 -1
  38. package/dist/mutator/block-pack/block_pack.js.map +1 -1
  39. package/dist/mutator/template/template_cache.cjs +515 -0
  40. package/dist/mutator/template/template_cache.cjs.map +1 -0
  41. package/dist/mutator/template/template_cache.d.ts +78 -0
  42. package/dist/mutator/template/template_cache.js +502 -0
  43. package/dist/mutator/template/template_cache.js.map +1 -0
  44. package/dist/mutator/template/template_loading.cjs +3 -1
  45. package/dist/mutator/template/template_loading.cjs.map +1 -1
  46. package/dist/mutator/template/template_loading.js +3 -1
  47. package/dist/mutator/template/template_loading.js.map +1 -1
  48. package/package.json +17 -17
  49. package/src/debug/index.ts +6 -0
  50. package/src/index.ts +1 -0
  51. package/src/js_render/computable_context.ts +13 -2
  52. package/src/js_render/context.ts +58 -5
  53. package/src/js_render/index.ts +8 -1
  54. package/src/middle_layer/project.ts +12 -8
  55. package/src/middle_layer/project_overview.ts +6 -0
  56. package/src/model/block_pack_spec.ts +2 -2
  57. package/src/model/template_spec.ts +11 -1
  58. package/src/mutator/block-pack/block_pack.ts +35 -1
  59. package/src/mutator/template/template_cache.test.ts +373 -0
  60. package/src/mutator/template/template_cache.ts +763 -0
  61. package/src/mutator/template/template_loading.ts +3 -0
@@ -30,7 +30,8 @@ async function prepareTemplateSpec(tpl) {
30
30
  content: await node_fs.default.promises.readFile(tpl.path)
31
31
  };
32
32
  case "from-registry":
33
- case "explicit": return tpl;
33
+ case "explicit":
34
+ case "cached": return tpl;
34
35
  case "prepared": return tpl;
35
36
  default: return (0, _milaboratories_ts_helpers.assertNever)(tpl);
36
37
  }
@@ -49,6 +50,7 @@ function loadTemplate(tx, spec) {
49
50
  case "from-registry": return loadTemplateFromRegistry(tx, spec);
50
51
  case "explicit": return require_direct_template_loader.loadTemplateFromExplicitDirect(tx, spec);
51
52
  case "prepared": return require_direct_template_loader.loadTemplateFromPrepared(tx, spec);
53
+ case "cached": return spec.resourceId;
52
54
  default: return (0, _milaboratories_ts_helpers.assertNever)(spec);
53
55
  }
54
56
  }
@@ -1 +1 @@
1
- {"version":3,"file":"template_loading.cjs","names":["fs","Pl","loadTemplateFromExplicitDirect","loadTemplateFromPrepared"],"sources":["../../../src/mutator/template/template_loading.ts"],"sourcesContent":["import type { AnyRef, PlTransaction, ResourceType } from \"@milaboratories/pl-client\";\nimport { field, Pl } from \"@milaboratories/pl-client\";\nimport fs from \"node:fs\";\nimport type {\n TemplateFromRegistry,\n TemplateSpecAny,\n TemplateSpecPrepared,\n} from \"../../model/template_spec\";\nimport { assertNever } from \"@milaboratories/ts-helpers\";\nimport { loadTemplateFromExplicitDirect, loadTemplateFromPrepared } from \"./direct_template_loader\";\n\n//\n// Resource schema\n//\n\nexport const TengoTemplateGet: ResourceType = { name: \"TengoTemplateGet\", version: \"1\" };\nexport const TengoTemplateGetRegistry = \"registry\";\nexport const TengoTemplateGetTemplateURI = \"templateURI\";\nexport const TengoTemplateGetTemplate = \"template\";\n\nexport const TengoTemplatePack: ResourceType = { name: \"TengoTemplatePack\", version: \"1\" };\nexport const TengoTemplatePackConvert: ResourceType = {\n name: \"TengoTemplatePackConvert\",\n version: \"1\",\n};\nexport const TengoTemplatePackConvertTemplatePack = \"templatePack\";\nexport const TengoTemplatePackConvertTemplate = \"template\";\n\nexport async function prepareTemplateSpec(tpl: TemplateSpecAny): Promise<TemplateSpecPrepared> {\n switch (tpl.type) {\n case \"from-file\":\n return {\n type: \"explicit\",\n content: await fs.promises.readFile(tpl.path),\n };\n case \"from-registry\":\n case \"explicit\":\n return tpl;\n case \"prepared\":\n return tpl;\n default:\n return assertNever(tpl);\n }\n}\n\nfunction loadTemplateFromRegistry(tx: PlTransaction, spec: TemplateFromRegistry): AnyRef {\n const getTemplate = tx.createStruct(TengoTemplateGet);\n const registry = field(getTemplate, TengoTemplateGetRegistry);\n const uri = field(getTemplate, TengoTemplateGetTemplateURI);\n const templateFromRegistry = field(getTemplate, TengoTemplateGetTemplate);\n\n // Note: it has a resource schema, so platforma creates fields by itself.\n\n tx.setField(registry, tx.createValue(Pl.JsonString, Buffer.from(JSON.stringify(spec.registry))));\n tx.setField(uri, tx.createValue(Pl.JsonString, Buffer.from(JSON.stringify(spec.path))));\n\n return templateFromRegistry;\n}\n\nexport function loadTemplate(tx: PlTransaction, spec: TemplateSpecPrepared): AnyRef {\n switch (spec.type) {\n case \"from-registry\":\n return loadTemplateFromRegistry(tx, spec);\n case \"explicit\":\n return loadTemplateFromExplicitDirect(tx, spec);\n case \"prepared\":\n return loadTemplateFromPrepared(tx, spec);\n default:\n return assertNever(spec);\n }\n}\n"],"mappings":";;;;;;;;AAeA,MAAa,mBAAiC;CAAE,MAAM;CAAoB,SAAS;CAAK;AACxF,MAAa,2BAA2B;AACxC,MAAa,8BAA8B;AAC3C,MAAa,2BAA2B;AAExC,MAAa,oBAAkC;CAAE,MAAM;CAAqB,SAAS;CAAK;AAC1F,MAAa,2BAAyC;CACpD,MAAM;CACN,SAAS;CACV;AACD,MAAa,uCAAuC;AACpD,MAAa,mCAAmC;AAEhD,eAAsB,oBAAoB,KAAqD;AAC7F,SAAQ,IAAI,MAAZ;EACE,KAAK,YACH,QAAO;GACL,MAAM;GACN,SAAS,MAAMA,gBAAG,SAAS,SAAS,IAAI,KAAK;GAC9C;EACH,KAAK;EACL,KAAK,WACH,QAAO;EACT,KAAK,WACH,QAAO;EACT,QACE,oDAAmB,IAAI;;;AAI7B,SAAS,yBAAyB,IAAmB,MAAoC;CACvF,MAAM,cAAc,GAAG,aAAa,iBAAiB;CACrD,MAAM,gDAAiB,aAAa,yBAAyB;CAC7D,MAAM,2CAAY,aAAa,4BAA4B;CAC3D,MAAM,4DAA6B,aAAa,yBAAyB;AAIzE,IAAG,SAAS,UAAU,GAAG,YAAYC,6BAAG,YAAY,OAAO,KAAK,KAAK,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC;AAChG,IAAG,SAAS,KAAK,GAAG,YAAYA,6BAAG,YAAY,OAAO,KAAK,KAAK,UAAU,KAAK,KAAK,CAAC,CAAC,CAAC;AAEvF,QAAO;;AAGT,SAAgB,aAAa,IAAmB,MAAoC;AAClF,SAAQ,KAAK,MAAb;EACE,KAAK,gBACH,QAAO,yBAAyB,IAAI,KAAK;EAC3C,KAAK,WACH,QAAOC,8DAA+B,IAAI,KAAK;EACjD,KAAK,WACH,QAAOC,wDAAyB,IAAI,KAAK;EAC3C,QACE,oDAAmB,KAAK"}
1
+ {"version":3,"file":"template_loading.cjs","names":["fs","Pl","loadTemplateFromExplicitDirect","loadTemplateFromPrepared"],"sources":["../../../src/mutator/template/template_loading.ts"],"sourcesContent":["import type { AnyRef, PlTransaction, ResourceType } from \"@milaboratories/pl-client\";\nimport { field, Pl } from \"@milaboratories/pl-client\";\nimport fs from \"node:fs\";\nimport type {\n TemplateFromRegistry,\n TemplateSpecAny,\n TemplateSpecPrepared,\n} from \"../../model/template_spec\";\nimport { assertNever } from \"@milaboratories/ts-helpers\";\nimport { loadTemplateFromExplicitDirect, loadTemplateFromPrepared } from \"./direct_template_loader\";\n\n//\n// Resource schema\n//\n\nexport const TengoTemplateGet: ResourceType = { name: \"TengoTemplateGet\", version: \"1\" };\nexport const TengoTemplateGetRegistry = \"registry\";\nexport const TengoTemplateGetTemplateURI = \"templateURI\";\nexport const TengoTemplateGetTemplate = \"template\";\n\nexport const TengoTemplatePack: ResourceType = { name: \"TengoTemplatePack\", version: \"1\" };\nexport const TengoTemplatePackConvert: ResourceType = {\n name: \"TengoTemplatePackConvert\",\n version: \"1\",\n};\nexport const TengoTemplatePackConvertTemplatePack = \"templatePack\";\nexport const TengoTemplatePackConvertTemplate = \"template\";\n\nexport async function prepareTemplateSpec(tpl: TemplateSpecAny): Promise<TemplateSpecPrepared> {\n switch (tpl.type) {\n case \"from-file\":\n return {\n type: \"explicit\",\n content: await fs.promises.readFile(tpl.path),\n };\n case \"from-registry\":\n case \"explicit\":\n case \"cached\":\n return tpl;\n case \"prepared\":\n return tpl;\n default:\n return assertNever(tpl);\n }\n}\n\nfunction loadTemplateFromRegistry(tx: PlTransaction, spec: TemplateFromRegistry): AnyRef {\n const getTemplate = tx.createStruct(TengoTemplateGet);\n const registry = field(getTemplate, TengoTemplateGetRegistry);\n const uri = field(getTemplate, TengoTemplateGetTemplateURI);\n const templateFromRegistry = field(getTemplate, TengoTemplateGetTemplate);\n\n // Note: it has a resource schema, so platforma creates fields by itself.\n\n tx.setField(registry, tx.createValue(Pl.JsonString, Buffer.from(JSON.stringify(spec.registry))));\n tx.setField(uri, tx.createValue(Pl.JsonString, Buffer.from(JSON.stringify(spec.path))));\n\n return templateFromRegistry;\n}\n\nexport function loadTemplate(tx: PlTransaction, spec: TemplateSpecPrepared): AnyRef {\n switch (spec.type) {\n case \"from-registry\":\n return loadTemplateFromRegistry(tx, spec);\n case \"explicit\":\n return loadTemplateFromExplicitDirect(tx, spec);\n case \"prepared\":\n return loadTemplateFromPrepared(tx, spec);\n case \"cached\":\n return spec.resourceId;\n default:\n return assertNever(spec);\n }\n}\n"],"mappings":";;;;;;;;AAeA,MAAa,mBAAiC;CAAE,MAAM;CAAoB,SAAS;CAAK;AACxF,MAAa,2BAA2B;AACxC,MAAa,8BAA8B;AAC3C,MAAa,2BAA2B;AAExC,MAAa,oBAAkC;CAAE,MAAM;CAAqB,SAAS;CAAK;AAC1F,MAAa,2BAAyC;CACpD,MAAM;CACN,SAAS;CACV;AACD,MAAa,uCAAuC;AACpD,MAAa,mCAAmC;AAEhD,eAAsB,oBAAoB,KAAqD;AAC7F,SAAQ,IAAI,MAAZ;EACE,KAAK,YACH,QAAO;GACL,MAAM;GACN,SAAS,MAAMA,gBAAG,SAAS,SAAS,IAAI,KAAK;GAC9C;EACH,KAAK;EACL,KAAK;EACL,KAAK,SACH,QAAO;EACT,KAAK,WACH,QAAO;EACT,QACE,oDAAmB,IAAI;;;AAI7B,SAAS,yBAAyB,IAAmB,MAAoC;CACvF,MAAM,cAAc,GAAG,aAAa,iBAAiB;CACrD,MAAM,gDAAiB,aAAa,yBAAyB;CAC7D,MAAM,2CAAY,aAAa,4BAA4B;CAC3D,MAAM,4DAA6B,aAAa,yBAAyB;AAIzE,IAAG,SAAS,UAAU,GAAG,YAAYC,6BAAG,YAAY,OAAO,KAAK,KAAK,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC;AAChG,IAAG,SAAS,KAAK,GAAG,YAAYA,6BAAG,YAAY,OAAO,KAAK,KAAK,UAAU,KAAK,KAAK,CAAC,CAAC,CAAC;AAEvF,QAAO;;AAGT,SAAgB,aAAa,IAAmB,MAAoC;AAClF,SAAQ,KAAK,MAAb;EACE,KAAK,gBACH,QAAO,yBAAyB,IAAI,KAAK;EAC3C,KAAK,WACH,QAAOC,8DAA+B,IAAI,KAAK;EACjD,KAAK,WACH,QAAOC,wDAAyB,IAAI,KAAK;EAC3C,KAAK,SACH,QAAO,KAAK;EACd,QACE,oDAAmB,KAAK"}
@@ -28,7 +28,8 @@ async function prepareTemplateSpec(tpl) {
28
28
  content: await fs.promises.readFile(tpl.path)
29
29
  };
30
30
  case "from-registry":
31
- case "explicit": return tpl;
31
+ case "explicit":
32
+ case "cached": return tpl;
32
33
  case "prepared": return tpl;
33
34
  default: return assertNever(tpl);
34
35
  }
@@ -47,6 +48,7 @@ function loadTemplate(tx, spec) {
47
48
  case "from-registry": return loadTemplateFromRegistry(tx, spec);
48
49
  case "explicit": return loadTemplateFromExplicitDirect(tx, spec);
49
50
  case "prepared": return loadTemplateFromPrepared(tx, spec);
51
+ case "cached": return spec.resourceId;
50
52
  default: return assertNever(spec);
51
53
  }
52
54
  }
@@ -1 +1 @@
1
- {"version":3,"file":"template_loading.js","names":[],"sources":["../../../src/mutator/template/template_loading.ts"],"sourcesContent":["import type { AnyRef, PlTransaction, ResourceType } from \"@milaboratories/pl-client\";\nimport { field, Pl } from \"@milaboratories/pl-client\";\nimport fs from \"node:fs\";\nimport type {\n TemplateFromRegistry,\n TemplateSpecAny,\n TemplateSpecPrepared,\n} from \"../../model/template_spec\";\nimport { assertNever } from \"@milaboratories/ts-helpers\";\nimport { loadTemplateFromExplicitDirect, loadTemplateFromPrepared } from \"./direct_template_loader\";\n\n//\n// Resource schema\n//\n\nexport const TengoTemplateGet: ResourceType = { name: \"TengoTemplateGet\", version: \"1\" };\nexport const TengoTemplateGetRegistry = \"registry\";\nexport const TengoTemplateGetTemplateURI = \"templateURI\";\nexport const TengoTemplateGetTemplate = \"template\";\n\nexport const TengoTemplatePack: ResourceType = { name: \"TengoTemplatePack\", version: \"1\" };\nexport const TengoTemplatePackConvert: ResourceType = {\n name: \"TengoTemplatePackConvert\",\n version: \"1\",\n};\nexport const TengoTemplatePackConvertTemplatePack = \"templatePack\";\nexport const TengoTemplatePackConvertTemplate = \"template\";\n\nexport async function prepareTemplateSpec(tpl: TemplateSpecAny): Promise<TemplateSpecPrepared> {\n switch (tpl.type) {\n case \"from-file\":\n return {\n type: \"explicit\",\n content: await fs.promises.readFile(tpl.path),\n };\n case \"from-registry\":\n case \"explicit\":\n return tpl;\n case \"prepared\":\n return tpl;\n default:\n return assertNever(tpl);\n }\n}\n\nfunction loadTemplateFromRegistry(tx: PlTransaction, spec: TemplateFromRegistry): AnyRef {\n const getTemplate = tx.createStruct(TengoTemplateGet);\n const registry = field(getTemplate, TengoTemplateGetRegistry);\n const uri = field(getTemplate, TengoTemplateGetTemplateURI);\n const templateFromRegistry = field(getTemplate, TengoTemplateGetTemplate);\n\n // Note: it has a resource schema, so platforma creates fields by itself.\n\n tx.setField(registry, tx.createValue(Pl.JsonString, Buffer.from(JSON.stringify(spec.registry))));\n tx.setField(uri, tx.createValue(Pl.JsonString, Buffer.from(JSON.stringify(spec.path))));\n\n return templateFromRegistry;\n}\n\nexport function loadTemplate(tx: PlTransaction, spec: TemplateSpecPrepared): AnyRef {\n switch (spec.type) {\n case \"from-registry\":\n return loadTemplateFromRegistry(tx, spec);\n case \"explicit\":\n return loadTemplateFromExplicitDirect(tx, spec);\n case \"prepared\":\n return loadTemplateFromPrepared(tx, spec);\n default:\n return assertNever(spec);\n }\n}\n"],"mappings":";;;;;;AAeA,MAAa,mBAAiC;CAAE,MAAM;CAAoB,SAAS;CAAK;AACxF,MAAa,2BAA2B;AACxC,MAAa,8BAA8B;AAC3C,MAAa,2BAA2B;AAExC,MAAa,oBAAkC;CAAE,MAAM;CAAqB,SAAS;CAAK;AAC1F,MAAa,2BAAyC;CACpD,MAAM;CACN,SAAS;CACV;AACD,MAAa,uCAAuC;AACpD,MAAa,mCAAmC;AAEhD,eAAsB,oBAAoB,KAAqD;AAC7F,SAAQ,IAAI,MAAZ;EACE,KAAK,YACH,QAAO;GACL,MAAM;GACN,SAAS,MAAM,GAAG,SAAS,SAAS,IAAI,KAAK;GAC9C;EACH,KAAK;EACL,KAAK,WACH,QAAO;EACT,KAAK,WACH,QAAO;EACT,QACE,QAAO,YAAY,IAAI;;;AAI7B,SAAS,yBAAyB,IAAmB,MAAoC;CACvF,MAAM,cAAc,GAAG,aAAa,iBAAiB;CACrD,MAAM,WAAW,MAAM,aAAa,yBAAyB;CAC7D,MAAM,MAAM,MAAM,aAAa,4BAA4B;CAC3D,MAAM,uBAAuB,MAAM,aAAa,yBAAyB;AAIzE,IAAG,SAAS,UAAU,GAAG,YAAY,GAAG,YAAY,OAAO,KAAK,KAAK,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC;AAChG,IAAG,SAAS,KAAK,GAAG,YAAY,GAAG,YAAY,OAAO,KAAK,KAAK,UAAU,KAAK,KAAK,CAAC,CAAC,CAAC;AAEvF,QAAO;;AAGT,SAAgB,aAAa,IAAmB,MAAoC;AAClF,SAAQ,KAAK,MAAb;EACE,KAAK,gBACH,QAAO,yBAAyB,IAAI,KAAK;EAC3C,KAAK,WACH,QAAO,+BAA+B,IAAI,KAAK;EACjD,KAAK,WACH,QAAO,yBAAyB,IAAI,KAAK;EAC3C,QACE,QAAO,YAAY,KAAK"}
1
+ {"version":3,"file":"template_loading.js","names":[],"sources":["../../../src/mutator/template/template_loading.ts"],"sourcesContent":["import type { AnyRef, PlTransaction, ResourceType } from \"@milaboratories/pl-client\";\nimport { field, Pl } from \"@milaboratories/pl-client\";\nimport fs from \"node:fs\";\nimport type {\n TemplateFromRegistry,\n TemplateSpecAny,\n TemplateSpecPrepared,\n} from \"../../model/template_spec\";\nimport { assertNever } from \"@milaboratories/ts-helpers\";\nimport { loadTemplateFromExplicitDirect, loadTemplateFromPrepared } from \"./direct_template_loader\";\n\n//\n// Resource schema\n//\n\nexport const TengoTemplateGet: ResourceType = { name: \"TengoTemplateGet\", version: \"1\" };\nexport const TengoTemplateGetRegistry = \"registry\";\nexport const TengoTemplateGetTemplateURI = \"templateURI\";\nexport const TengoTemplateGetTemplate = \"template\";\n\nexport const TengoTemplatePack: ResourceType = { name: \"TengoTemplatePack\", version: \"1\" };\nexport const TengoTemplatePackConvert: ResourceType = {\n name: \"TengoTemplatePackConvert\",\n version: \"1\",\n};\nexport const TengoTemplatePackConvertTemplatePack = \"templatePack\";\nexport const TengoTemplatePackConvertTemplate = \"template\";\n\nexport async function prepareTemplateSpec(tpl: TemplateSpecAny): Promise<TemplateSpecPrepared> {\n switch (tpl.type) {\n case \"from-file\":\n return {\n type: \"explicit\",\n content: await fs.promises.readFile(tpl.path),\n };\n case \"from-registry\":\n case \"explicit\":\n case \"cached\":\n return tpl;\n case \"prepared\":\n return tpl;\n default:\n return assertNever(tpl);\n }\n}\n\nfunction loadTemplateFromRegistry(tx: PlTransaction, spec: TemplateFromRegistry): AnyRef {\n const getTemplate = tx.createStruct(TengoTemplateGet);\n const registry = field(getTemplate, TengoTemplateGetRegistry);\n const uri = field(getTemplate, TengoTemplateGetTemplateURI);\n const templateFromRegistry = field(getTemplate, TengoTemplateGetTemplate);\n\n // Note: it has a resource schema, so platforma creates fields by itself.\n\n tx.setField(registry, tx.createValue(Pl.JsonString, Buffer.from(JSON.stringify(spec.registry))));\n tx.setField(uri, tx.createValue(Pl.JsonString, Buffer.from(JSON.stringify(spec.path))));\n\n return templateFromRegistry;\n}\n\nexport function loadTemplate(tx: PlTransaction, spec: TemplateSpecPrepared): AnyRef {\n switch (spec.type) {\n case \"from-registry\":\n return loadTemplateFromRegistry(tx, spec);\n case \"explicit\":\n return loadTemplateFromExplicitDirect(tx, spec);\n case \"prepared\":\n return loadTemplateFromPrepared(tx, spec);\n case \"cached\":\n return spec.resourceId;\n default:\n return assertNever(spec);\n }\n}\n"],"mappings":";;;;;;AAeA,MAAa,mBAAiC;CAAE,MAAM;CAAoB,SAAS;CAAK;AACxF,MAAa,2BAA2B;AACxC,MAAa,8BAA8B;AAC3C,MAAa,2BAA2B;AAExC,MAAa,oBAAkC;CAAE,MAAM;CAAqB,SAAS;CAAK;AAC1F,MAAa,2BAAyC;CACpD,MAAM;CACN,SAAS;CACV;AACD,MAAa,uCAAuC;AACpD,MAAa,mCAAmC;AAEhD,eAAsB,oBAAoB,KAAqD;AAC7F,SAAQ,IAAI,MAAZ;EACE,KAAK,YACH,QAAO;GACL,MAAM;GACN,SAAS,MAAM,GAAG,SAAS,SAAS,IAAI,KAAK;GAC9C;EACH,KAAK;EACL,KAAK;EACL,KAAK,SACH,QAAO;EACT,KAAK,WACH,QAAO;EACT,QACE,QAAO,YAAY,IAAI;;;AAI7B,SAAS,yBAAyB,IAAmB,MAAoC;CACvF,MAAM,cAAc,GAAG,aAAa,iBAAiB;CACrD,MAAM,WAAW,MAAM,aAAa,yBAAyB;CAC7D,MAAM,MAAM,MAAM,aAAa,4BAA4B;CAC3D,MAAM,uBAAuB,MAAM,aAAa,yBAAyB;AAIzE,IAAG,SAAS,UAAU,GAAG,YAAY,GAAG,YAAY,OAAO,KAAK,KAAK,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC;AAChG,IAAG,SAAS,KAAK,GAAG,YAAY,GAAG,YAAY,OAAO,KAAK,KAAK,UAAU,KAAK,KAAK,CAAC,CAAC,CAAC;AAEvF,QAAO;;AAGT,SAAgB,aAAa,IAAmB,MAAoC;AAClF,SAAQ,KAAK,MAAb;EACE,KAAK,gBACH,QAAO,yBAAyB,IAAI,KAAK;EAC3C,KAAK,WACH,QAAO,+BAA+B,IAAI,KAAK;EACjD,KAAK,WACH,QAAO,yBAAyB,IAAI,KAAK;EAC3C,KAAK,SACH,QAAO,KAAK;EACd,QACE,QAAO,YAAY,KAAK"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milaboratories/pl-middle-layer",
3
- "version": "1.51.0",
3
+ "version": "1.52.1",
4
4
  "description": "Pl Middle Layer",
5
5
  "keywords": [],
6
6
  "license": "UNLICENSED",
@@ -30,22 +30,22 @@
30
30
  "utility-types": "^3.11.0",
31
31
  "yaml": "^2.8.0",
32
32
  "zod": "~3.23.8",
33
- "@milaboratories/pf-driver": "1.1.0",
34
- "@milaboratories/pl-client": "2.18.0",
35
- "@milaboratories/pl-drivers": "1.12.0",
36
- "@milaboratories/pl-errors": "1.2.0",
37
- "@milaboratories/pl-model-backend": "1.2.0",
38
- "@milaboratories/pl-deployments": "2.16.0",
39
- "@milaboratories/pl-model-middle-layer": "1.14.0",
40
- "@milaboratories/computable": "2.8.6",
41
- "@milaboratories/pl-model-common": "1.27.0",
33
+ "@milaboratories/pf-driver": "1.1.1",
34
+ "@milaboratories/computable": "2.9.0",
35
+ "@milaboratories/pl-client": "2.18.1",
36
+ "@milaboratories/pl-deployments": "2.16.1",
37
+ "@milaboratories/pl-errors": "1.2.1",
42
38
  "@milaboratories/pl-http": "1.2.4",
43
- "@milaboratories/resolve-helper": "1.1.3",
44
- "@milaboratories/pl-tree": "1.9.0",
39
+ "@milaboratories/pl-drivers": "1.12.2",
40
+ "@milaboratories/pl-model-common": "1.28.0",
41
+ "@milaboratories/pl-model-backend": "1.2.1",
42
+ "@milaboratories/pl-model-middle-layer": "1.15.0",
43
+ "@milaboratories/pl-tree": "1.9.2",
45
44
  "@milaboratories/ts-helpers": "1.7.3",
46
- "@platforma-sdk/model": "1.60.0",
47
- "@platforma-sdk/block-tools": "2.7.0",
48
- "@platforma-sdk/workflow-tengo": "5.11.0"
45
+ "@milaboratories/resolve-helper": "1.1.3",
46
+ "@platforma-sdk/block-tools": "2.7.1",
47
+ "@platforma-sdk/workflow-tengo": "5.11.0",
48
+ "@platforma-sdk/model": "1.60.2"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/node": "~24.5.2",
@@ -54,9 +54,9 @@
54
54
  "semver": "^7.7.2",
55
55
  "typescript": "~5.9.3",
56
56
  "vitest": "^4.0.18",
57
- "@milaboratories/ts-builder": "1.3.0",
58
57
  "@milaboratories/ts-configs": "1.2.2",
59
- "@milaboratories/build-configs": "1.5.2"
58
+ "@milaboratories/build-configs": "1.5.2",
59
+ "@milaboratories/ts-builder": "1.3.0"
60
60
  },
61
61
  "engines": {
62
62
  "node": ">=22.19.0"
@@ -1,9 +1,12 @@
1
1
  export type MlDebugFlags = {
2
2
  logTreeStats?: "cumulative" | "per-request";
3
3
  logProjectMutationStat: boolean;
4
+ logTemplateCacheStat: boolean;
4
5
  dumpInitialTreeState: boolean;
5
6
  logOutputStatus?: "any" | "unstable-only";
6
7
  logOutputRecalculations?: boolean;
8
+ logProjectOverviewStat: boolean;
9
+ logJsExecStat: boolean;
7
10
  };
8
11
 
9
12
  let flags: MlDebugFlags | undefined = undefined;
@@ -12,7 +15,10 @@ export function getDebugFlags() {
12
15
  flags = {
13
16
  dumpInitialTreeState: process.env.MI_DUMP_INITIAL_TREE_STATE !== undefined,
14
17
  logProjectMutationStat: process.env.MI_LOG_PROJECT_MUTATION_STAT !== undefined,
18
+ logTemplateCacheStat: process.env.MI_LOG_TEMPLATE_CACHE_STAT !== undefined,
15
19
  logOutputRecalculations: process.env.MI_LOG_OUTPUT_RECALCULATIONS !== undefined,
20
+ logProjectOverviewStat: process.env.MI_LOG_PROJECT_OVERVIEW_STAT !== undefined,
21
+ logJsExecStat: process.env.MI_LOG_JS_EXEC_STAT !== undefined,
16
22
  };
17
23
  if (process.env.MI_LOG_OUTPUT_STATUS)
18
24
  flags.logOutputStatus =
package/src/index.ts CHANGED
@@ -19,6 +19,7 @@ export type { InternalLsDriver } from "@milaboratories/pl-drivers";
19
19
 
20
20
  // for tests etc..
21
21
  export * from "./mutator/template/template_loading";
22
+ export * from "./mutator/template/template_cache";
22
23
  export * from "./mutator/template/render_template";
23
24
  export * from "./model/template_spec";
24
25
 
@@ -66,7 +66,15 @@ export class ComputableContextHelper implements JsRenderInternal.GlobalCfgRender
66
66
  private readonly accessors = new Map<string, PlTreeNodeAccessor | undefined>();
67
67
  private readonly specDriver = new SpecDriver();
68
68
 
69
- private readonly meta: Map<string, Block>;
69
+ private _meta: Map<string, Block> | undefined;
70
+ private get meta(): Map<string, Block> {
71
+ if (this._meta === undefined) {
72
+ if (this.computableCtx === undefined)
73
+ throw new Error("blockMeta can't be resolved in this context");
74
+ this._meta = this.blockCtx.blockMeta(this.computableCtx);
75
+ }
76
+ return this._meta;
77
+ }
70
78
 
71
79
  constructor(
72
80
  private readonly parent: JsExecutionContext,
@@ -76,7 +84,6 @@ export class ComputableContextHelper implements JsRenderInternal.GlobalCfgRender
76
84
  computableCtx: ComputableCtx,
77
85
  ) {
78
86
  this.computableCtx = computableCtx;
79
- this.meta = blockCtx.blockMeta(computableCtx);
80
87
  }
81
88
 
82
89
  public resetComputableCtx() {
@@ -533,11 +540,15 @@ export class ComputableContextHelper implements JsRenderInternal.GlobalCfgRender
533
540
  // QuickJS strips all fields from errors apart from 'name' and 'message'.
534
541
  // That's why here we need to store them, and rethrow them when we exit
535
542
  // from QuickJS code.
543
+ const t0 = performance.now();
536
544
  try {
537
545
  return (fn as any)(...args);
538
546
  } catch (e: unknown) {
539
547
  const newErr = parent.errorRepo.setAndRecreateForQuickJS(e);
540
548
  throw vm.newError(newErr);
549
+ } finally {
550
+ parent.stats.ctxMethodCalls++;
551
+ parent.stats.ctxMethodMs += performance.now() - t0;
541
552
  }
542
553
  };
543
554
 
@@ -39,6 +39,26 @@ export type ComputableEnv = {
39
39
  computableCtx: ComputableCtx;
40
40
  };
41
41
 
42
+ /** Execution stats accumulated during the lifetime of a JsExecutionContext. */
43
+ export type JsExecStats = {
44
+ bundleEvalMs: number;
45
+ bundleBytes: number;
46
+
47
+ callbackMs: number;
48
+ callbackCount: number;
49
+
50
+ serInMs: number;
51
+ serInBytes: number;
52
+ serInCalls: number;
53
+
54
+ serOutMs: number;
55
+ serOutBytes: number;
56
+ serOutCalls: number;
57
+
58
+ ctxMethodCalls: number;
59
+ ctxMethodMs: number;
60
+ };
61
+
42
62
  export class JsExecutionContext {
43
63
  private readonly callbackRegistry: QuickJSHandle;
44
64
  private readonly fnJSONStringify: QuickJSHandle;
@@ -48,6 +68,21 @@ export class JsExecutionContext {
48
68
 
49
69
  public readonly computableHelper: ComputableContextHelper | undefined;
50
70
 
71
+ public readonly stats: JsExecStats = {
72
+ bundleEvalMs: 0,
73
+ bundleBytes: 0,
74
+ callbackMs: 0,
75
+ callbackCount: 0,
76
+ serInMs: 0,
77
+ serInBytes: 0,
78
+ serInCalls: 0,
79
+ serOutMs: 0,
80
+ serOutBytes: 0,
81
+ serOutCalls: 0,
82
+ ctxMethodCalls: 0,
83
+ ctxMethodMs: 0,
84
+ };
85
+
51
86
  /**
52
87
  * Creates a new JS execution context.
53
88
  *
@@ -111,6 +146,7 @@ export class JsExecutionContext {
111
146
  // }
112
147
 
113
148
  public evaluateBundle(code: string) {
149
+ const t0 = performance.now();
114
150
  try {
115
151
  this.deadlineSetter({
116
152
  currentExecutionTarget: "evaluateBundle",
@@ -122,10 +158,13 @@ export class JsExecutionContext {
122
158
  throw err;
123
159
  } finally {
124
160
  this.deadlineSetter(undefined);
161
+ this.stats.bundleEvalMs += performance.now() - t0;
162
+ this.stats.bundleBytes += code.length;
125
163
  }
126
164
  }
127
165
 
128
166
  public runCallback(cbName: string, ...args: unknown[]): QuickJSHandle {
167
+ const t0 = performance.now();
129
168
  try {
130
169
  this.deadlineSetter({ currentExecutionTarget: cbName, deadline: Date.now() + 10000 });
131
170
  return Scope.withScope((localScope) => {
@@ -150,6 +189,8 @@ export class JsExecutionContext {
150
189
  throw original;
151
190
  } finally {
152
191
  this.deadlineSetter(undefined);
192
+ this.stats.callbackMs += performance.now() - t0;
193
+ this.stats.callbackCount++;
153
194
  }
154
195
  }
155
196
 
@@ -210,11 +251,16 @@ export class JsExecutionContext {
210
251
  }
211
252
 
212
253
  public exportObjectViaJson(obj: unknown, scope: Scope | undefined): QuickJSHandle {
254
+ const t0 = performance.now();
255
+ const json = JSON.stringify(obj);
256
+ this.stats.serInBytes += json.length;
257
+ this.stats.serInCalls++;
213
258
  const result = this.vm
214
- .newString(JSON.stringify(obj))
215
- .consume((json) =>
216
- this.vm.unwrapResult(this.vm.callFunction(this.fnJSONParse, this.vm.undefined, json)),
259
+ .newString(json)
260
+ .consume((jsonHandle) =>
261
+ this.vm.unwrapResult(this.vm.callFunction(this.fnJSONParse, this.vm.undefined, jsonHandle)),
217
262
  );
263
+ this.stats.serInMs += performance.now() - t0;
218
264
  return scope !== undefined ? scope.manage(result) : result;
219
265
  }
220
266
 
@@ -233,13 +279,20 @@ export class JsExecutionContext {
233
279
  }
234
280
 
235
281
  public importObjectViaJson(handle: QuickJSHandle): unknown {
282
+ const t0 = performance.now();
236
283
  const text = this.vm
237
284
  .unwrapResult(this.vm.callFunction(this.fnJSONStringify, this.vm.undefined, handle))
238
285
  .consume((strHandle) => this.vm.getString(strHandle));
239
- if (text === "undefined")
286
+ this.stats.serOutBytes += text.length;
287
+ this.stats.serOutCalls++;
288
+ if (text === "undefined") {
240
289
  // special case with futures
290
+ this.stats.serOutMs += performance.now() - t0;
241
291
  return undefined;
242
- return JSON.parse(text);
292
+ }
293
+ const result = JSON.parse(text);
294
+ this.stats.serOutMs += performance.now() - t0;
295
+ return result;
243
296
  }
244
297
 
245
298
  private injectCtx() {
@@ -135,6 +135,8 @@ export function computableFromRF(
135
135
 
136
136
  if (Object.keys(toBeResolved).length === 0) {
137
137
  const importedResult = rCtx.importObjectUniversal(result);
138
+ if (getDebugFlags().logJsExecStat)
139
+ console.log(`[jsExec] ${key}: ${JSON.stringify(rCtx.stats)}`);
138
140
  logOutputStatus(
139
141
  fh.handle,
140
142
  importedResult,
@@ -161,6 +163,8 @@ export function computableFromRF(
161
163
 
162
164
  // logging
163
165
  recalculationCounter++;
166
+ if (getDebugFlags().logJsExecStat)
167
+ console.log(`[jsExec] ${key} #${recalculationCounter}: ${JSON.stringify(rCtx.stats)}`);
164
168
  logOutputStatus(fh.handle, renderedResult, stable, recalculationCounter, unstableMarker);
165
169
 
166
170
  return renderedResult;
@@ -208,7 +212,10 @@ export function executeSingleLambda(
208
212
  rCtx.evaluateBundle(code.content);
209
213
 
210
214
  // Running the lambda with arguments (e.g., state for args(), args for enrichmentTargets())
211
- return rCtx.importObjectUniversal(rCtx.runCallback(fh.handle, ...args));
215
+ const importedResult = rCtx.importObjectUniversal(rCtx.runCallback(fh.handle, ...args));
216
+ if (getDebugFlags().logJsExecStat)
217
+ console.log(`[jsExec] ${fh.handle}: ${JSON.stringify(rCtx.stats)}`);
218
+ return importedResult;
212
219
  } finally {
213
220
  scope.dispose();
214
221
  }
@@ -47,6 +47,7 @@ import canonicalize from "canonicalize";
47
47
  import type { ProjectOverviewLight } from "./project_overview_light";
48
48
  import { projectOverviewLight } from "./project_overview_light";
49
49
  import { applyProjectMigrations } from "../mutator/migration";
50
+ import { cacheBlockPackTemplate } from "../mutator/template/template_cache";
50
51
 
51
52
  type BlockStateComputables = {
52
53
  readonly fullState: Computable<BlockStateInternalV3>;
@@ -210,18 +211,20 @@ export class Project {
210
211
  blockId: string = randomUUID(),
211
212
  ): Promise<string> {
212
213
  const preparedBp = await this.env.bpPreparer.prepare(blockPackSpec);
213
- const blockCfgContainer = await this.env.bpPreparer.getBlockConfigContainer(blockPackSpec);
214
- const blockCfg = extractConfig(blockCfgContainer); // full content of this var should never be persisted
214
+ const blockCfg = extractConfig(preparedBp.config);
215
215
 
216
216
  this.env.runtimeCapabilities.throwIfIncompatible(blockCfg.featureFlags);
217
217
 
218
+ // Pre-materialize template via cache (separate transaction(s))
219
+ const cachedBp = await cacheBlockPackTemplate(this.env.pl, preparedBp);
220
+
218
221
  // Build NewBlockSpec based on model API version
219
222
  const newBlockSpec =
220
223
  blockCfg.modelAPIVersion === BLOCK_STORAGE_FACADE_VERSION
221
- ? { storageMode: "fromModel" as const, blockPack: preparedBp }
224
+ ? { storageMode: "fromModel" as const, blockPack: cachedBp }
222
225
  : {
223
226
  storageMode: "legacy" as const,
224
- blockPack: preparedBp,
227
+ blockPack: cachedBp,
225
228
  legacyState: canonicalize({
226
229
  args: blockCfg.initialArgs,
227
230
  uiState: blockCfg.initialUiState,
@@ -301,12 +304,13 @@ export class Project {
301
304
  author?: AuthorMarker,
302
305
  ): Promise<void> {
303
306
  const preparedBp = await this.env.bpPreparer.prepare(blockPackSpec);
304
- const blockCfg = extractConfig(
305
- await this.env.bpPreparer.getBlockConfigContainer(blockPackSpec),
306
- );
307
+ const blockCfg = extractConfig(preparedBp.config);
307
308
 
308
309
  this.env.runtimeCapabilities.throwIfIncompatible(blockCfg.featureFlags);
309
310
 
311
+ // Pre-materialize template via cache (separate transaction(s))
312
+ const cachedBp = await cacheBlockPackTemplate(this.env.pl, preparedBp);
313
+
310
314
  // resetState signals to mutator to reset storage
311
315
  // For v2+ blocks: mutator gets initial storage directly via getInitialStorageInVM
312
316
  // For v1 blocks: we pass the legacy state format
@@ -323,7 +327,7 @@ export class Project {
323
327
  this.env.pl,
324
328
  this.rid,
325
329
  author,
326
- (mut) => mut.migrateBlockPack(blockId, preparedBp, resetState),
330
+ (mut) => mut.migrateBlockPack(blockId, cachedBp, resetState),
327
331
  { name: "updateBlockPack", lockId: this.projectLockId },
328
332
  );
329
333
  await this.projectTree.refreshState();
@@ -30,6 +30,7 @@ import type { NavigationStates } from "./navigation_states";
30
30
  import { getBlockPackInfo } from "./util";
31
31
  import { resourceIdToString, type ResourceId } from "@milaboratories/pl-client";
32
32
  import { omitBy, isEqual } from "es-toolkit";
33
+ import { getDebugFlags } from "../debug";
33
34
 
34
35
  type BlockInfo = {
35
36
  argsRid?: ResourceId;
@@ -346,6 +347,11 @@ export function projectOverview(
346
347
  }),
347
348
  };
348
349
  },
350
+ onRecalculation: getDebugFlags().logProjectOverviewStat
351
+ ? (stats) => {
352
+ console.log(`[projectOverview] ${JSON.stringify(stats)}`);
353
+ }
354
+ : undefined,
349
355
  },
350
356
  ).withStableType();
351
357
  }
@@ -1,4 +1,4 @@
1
- import type { ExplicitTemplate, PreparedTemplate } from "./template_spec";
1
+ import type { CachedTemplate, ExplicitTemplate, PreparedTemplate } from "./template_spec";
2
2
  import type { ResourceType } from "@milaboratories/pl-client";
3
3
  import type { BlockConfigContainer } from "@platforma-sdk/model";
4
4
  import type { BlockPackSpec } from "@milaboratories/pl-model-middle-layer";
@@ -48,7 +48,7 @@ export interface BlockPackExplicit {
48
48
  /** Block-pack spec that can be materialized in pl. */
49
49
  export type BlockPackSpecPrepared = {
50
50
  type: "prepared";
51
- template: PreparedTemplate;
51
+ template: PreparedTemplate | CachedTemplate;
52
52
  config: BlockConfigContainer;
53
53
  frontend: FrontendSpec;
54
54
  source: BlockPackSpec;
@@ -1,3 +1,4 @@
1
+ import type { ResourceId } from "@milaboratories/pl-client";
1
2
  import type { CompiledTemplateV3, TemplateData } from "@milaboratories/pl-model-backend";
2
3
 
3
4
  export interface TemplateFromRegistry {
@@ -16,10 +17,19 @@ export interface PreparedTemplate {
16
17
  data: TemplateData | CompiledTemplateV3;
17
18
  }
18
19
 
20
+ export interface CachedTemplate {
21
+ readonly type: "cached";
22
+ readonly resourceId: ResourceId;
23
+ }
24
+
19
25
  export interface TemplateFromFile {
20
26
  readonly type: "from-file";
21
27
  path: string;
22
28
  }
23
29
 
24
- export type TemplateSpecPrepared = TemplateFromRegistry | ExplicitTemplate | PreparedTemplate;
30
+ export type TemplateSpecPrepared =
31
+ | TemplateFromRegistry
32
+ | ExplicitTemplate
33
+ | PreparedTemplate
34
+ | CachedTemplate;
25
35
  export type TemplateSpecAny = TemplateSpecPrepared | TemplateFromFile;
@@ -4,6 +4,7 @@ import { loadTemplate } from "../template/template_loading";
4
4
  import type { BlockPackExplicit, BlockPackSpecAny, BlockPackSpecPrepared } from "../../model";
5
5
  import type { Signer } from "@milaboratories/ts-helpers";
6
6
  import { assertNever } from "@milaboratories/ts-helpers";
7
+ import type { Branded } from "@milaboratories/pl-model-common";
7
8
  import fs from "node:fs";
8
9
  import type { Dispatcher } from "undici";
9
10
  import { request } from "undici";
@@ -16,10 +17,13 @@ import { resolveDevPacket } from "../../dev_env";
16
17
  import { getDevV2PacketMtime } from "../../block_registry";
17
18
  import type { V2RegistryProvider } from "../../block_registry/registry-v2-provider";
18
19
  import { LRUCache } from "lru-cache";
20
+ import canonicalize from "canonicalize";
19
21
  import type { BlockPackSpec } from "@milaboratories/pl-model-middle-layer";
20
22
  import { WorkerManager } from "../../worker/WorkerManager";
21
23
  import { z } from "zod";
22
24
 
25
+ type PreparedCacheKey = Branded<string, "PreparedCacheKey">;
26
+
23
27
  export const BlockPackCustomType: ResourceType = { name: "BlockPackCustom", version: "1" };
24
28
  export const BlockPackTemplateField = "template";
25
29
  export const BlockPackFrontendField = "frontend";
@@ -65,6 +69,11 @@ export class BlockPackPreparer {
65
69
  sizeCalculation: (value) => value.byteLength,
66
70
  });
67
71
 
72
+ /** Cache of prepared block packs for registry specs (immutable by version). */
73
+ private readonly preparedCache = new LRUCache<PreparedCacheKey, BlockPackSpecPrepared>({
74
+ max: 50,
75
+ });
76
+
68
77
  public async getBlockConfigContainer(spec: BlockPackSpecAny): Promise<BlockConfigContainer> {
69
78
  switch (spec.type) {
70
79
  case "explicit":
@@ -106,16 +115,35 @@ export class BlockPackPreparer {
106
115
  }
107
116
  }
108
117
 
118
+ /** Returns a stable cache key for registry specs (immutable by version). Dev specs return undefined. */
119
+ private specKey(spec: BlockPackSpecAny): PreparedCacheKey | undefined {
120
+ switch (spec.type) {
121
+ case "from-registry-v1":
122
+ return `v1:${spec.registryUrl}:${spec.id.organization}:${spec.id.name}:${spec.id.version}` as PreparedCacheKey;
123
+ case "from-registry-v2":
124
+ return `v2:${spec.registryUrl}:${canonicalize(spec.id)}` as PreparedCacheKey;
125
+ default:
126
+ return undefined; // dev, explicit, prepared — not cacheable
127
+ }
128
+ }
129
+
109
130
  public async prepare(spec: BlockPackSpecAny): Promise<BlockPackSpecPrepared> {
110
131
  if (spec.type === "prepared") {
111
132
  return spec;
112
133
  }
113
134
 
135
+ // Check prepare cache for registry specs
136
+ const key = this.specKey(spec);
137
+ if (key) {
138
+ const cached = this.preparedCache.get(key);
139
+ if (cached) return cached;
140
+ }
141
+
114
142
  const explicit = await this.prepareWithoutUnpacking(spec);
115
143
 
116
144
  await using workerManager = new WorkerManager();
117
145
 
118
- return {
146
+ const result: BlockPackSpecPrepared = {
119
147
  ...explicit,
120
148
  type: "prepared",
121
149
  template: {
@@ -123,6 +151,12 @@ export class BlockPackPreparer {
123
151
  data: await workerManager.process("parseTemplate", explicit.template.content),
124
152
  },
125
153
  };
154
+
155
+ if (key) {
156
+ this.preparedCache.set(key, result);
157
+ }
158
+
159
+ return result;
126
160
  }
127
161
 
128
162
  private async prepareWithoutUnpacking(