@milaboratories/pl-middle-layer 1.45.5 → 1.46.0

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 (132) hide show
  1. package/dist/index.cjs +58 -0
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.js +2 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/js_render/computable_context.cjs +37 -7
  6. package/dist/js_render/computable_context.cjs.map +1 -1
  7. package/dist/js_render/computable_context.d.ts.map +1 -1
  8. package/dist/js_render/computable_context.js +37 -7
  9. package/dist/js_render/computable_context.js.map +1 -1
  10. package/dist/js_render/context.cjs +12 -4
  11. package/dist/js_render/context.cjs.map +1 -1
  12. package/dist/js_render/context.d.ts +9 -0
  13. package/dist/js_render/context.d.ts.map +1 -1
  14. package/dist/js_render/context.js +12 -4
  15. package/dist/js_render/context.js.map +1 -1
  16. package/dist/js_render/index.cjs +1 -1
  17. package/dist/js_render/index.cjs.map +1 -1
  18. package/dist/js_render/index.js +1 -1
  19. package/dist/js_render/index.js.map +1 -1
  20. package/dist/middle_layer/block.cjs +7 -8
  21. package/dist/middle_layer/block.cjs.map +1 -1
  22. package/dist/middle_layer/block.d.ts +4 -4
  23. package/dist/middle_layer/block.d.ts.map +1 -1
  24. package/dist/middle_layer/block.js +7 -8
  25. package/dist/middle_layer/block.js.map +1 -1
  26. package/dist/middle_layer/block_ctx.cjs +67 -13
  27. package/dist/middle_layer/block_ctx.cjs.map +1 -1
  28. package/dist/middle_layer/block_ctx.d.ts +4 -7
  29. package/dist/middle_layer/block_ctx.d.ts.map +1 -1
  30. package/dist/middle_layer/block_ctx.js +68 -14
  31. package/dist/middle_layer/block_ctx.js.map +1 -1
  32. package/dist/middle_layer/block_ctx_unsafe.cjs +10 -3
  33. package/dist/middle_layer/block_ctx_unsafe.cjs.map +1 -1
  34. package/dist/middle_layer/block_ctx_unsafe.d.ts +1 -1
  35. package/dist/middle_layer/block_ctx_unsafe.d.ts.map +1 -1
  36. package/dist/middle_layer/block_ctx_unsafe.js +10 -3
  37. package/dist/middle_layer/block_ctx_unsafe.js.map +1 -1
  38. package/dist/middle_layer/frontend_path.cjs +1 -0
  39. package/dist/middle_layer/frontend_path.cjs.map +1 -1
  40. package/dist/middle_layer/frontend_path.js +1 -0
  41. package/dist/middle_layer/frontend_path.js.map +1 -1
  42. package/dist/middle_layer/middle_layer.cjs +1 -0
  43. package/dist/middle_layer/middle_layer.cjs.map +1 -1
  44. package/dist/middle_layer/middle_layer.d.ts +1 -1
  45. package/dist/middle_layer/middle_layer.d.ts.map +1 -1
  46. package/dist/middle_layer/middle_layer.js +1 -0
  47. package/dist/middle_layer/middle_layer.js.map +1 -1
  48. package/dist/middle_layer/project.cjs +75 -28
  49. package/dist/middle_layer/project.cjs.map +1 -1
  50. package/dist/middle_layer/project.d.ts +34 -7
  51. package/dist/middle_layer/project.d.ts.map +1 -1
  52. package/dist/middle_layer/project.js +76 -29
  53. package/dist/middle_layer/project.js.map +1 -1
  54. package/dist/middle_layer/project_overview.cjs +32 -11
  55. package/dist/middle_layer/project_overview.cjs.map +1 -1
  56. package/dist/middle_layer/project_overview.d.ts.map +1 -1
  57. package/dist/middle_layer/project_overview.js +32 -11
  58. package/dist/middle_layer/project_overview.js.map +1 -1
  59. package/dist/middle_layer/render.cjs +1 -1
  60. package/dist/middle_layer/render.cjs.map +1 -1
  61. package/dist/middle_layer/render.js +1 -1
  62. package/dist/middle_layer/render.js.map +1 -1
  63. package/dist/middle_layer/render.test.d.ts.map +1 -1
  64. package/dist/model/block_storage_helper.cjs +210 -0
  65. package/dist/model/block_storage_helper.cjs.map +1 -0
  66. package/dist/model/block_storage_helper.d.ts +98 -0
  67. package/dist/model/block_storage_helper.d.ts.map +1 -0
  68. package/dist/model/block_storage_helper.js +153 -0
  69. package/dist/model/block_storage_helper.js.map +1 -0
  70. package/dist/model/index.d.ts +2 -1
  71. package/dist/model/index.d.ts.map +1 -1
  72. package/dist/model/project_helper.cjs +177 -0
  73. package/dist/model/project_helper.cjs.map +1 -1
  74. package/dist/model/project_helper.d.ts +110 -1
  75. package/dist/model/project_helper.d.ts.map +1 -1
  76. package/dist/model/project_helper.js +178 -1
  77. package/dist/model/project_helper.js.map +1 -1
  78. package/dist/model/project_model.cjs +6 -3
  79. package/dist/model/project_model.cjs.map +1 -1
  80. package/dist/model/project_model.d.ts +3 -2
  81. package/dist/model/project_model.d.ts.map +1 -1
  82. package/dist/model/project_model.js +6 -4
  83. package/dist/model/project_model.js.map +1 -1
  84. package/dist/mutator/block-pack/block_pack.cjs +1 -2
  85. package/dist/mutator/block-pack/block_pack.cjs.map +1 -1
  86. package/dist/mutator/block-pack/block_pack.d.ts.map +1 -1
  87. package/dist/mutator/block-pack/block_pack.js +1 -2
  88. package/dist/mutator/block-pack/block_pack.js.map +1 -1
  89. package/dist/mutator/block-pack/frontend.cjs +1 -0
  90. package/dist/mutator/block-pack/frontend.cjs.map +1 -1
  91. package/dist/mutator/block-pack/frontend.js +1 -0
  92. package/dist/mutator/block-pack/frontend.js.map +1 -1
  93. package/dist/mutator/migration.cjs +64 -3
  94. package/dist/mutator/migration.cjs.map +1 -1
  95. package/dist/mutator/migration.d.ts.map +1 -1
  96. package/dist/mutator/migration.js +66 -5
  97. package/dist/mutator/migration.js.map +1 -1
  98. package/dist/mutator/project-v3.test.d.ts +2 -0
  99. package/dist/mutator/project-v3.test.d.ts.map +1 -0
  100. package/dist/mutator/project.cjs +282 -41
  101. package/dist/mutator/project.cjs.map +1 -1
  102. package/dist/mutator/project.d.ts +77 -12
  103. package/dist/mutator/project.d.ts.map +1 -1
  104. package/dist/mutator/project.js +283 -42
  105. package/dist/mutator/project.js.map +1 -1
  106. package/dist/pool/result_pool.cjs +9 -6
  107. package/dist/pool/result_pool.cjs.map +1 -1
  108. package/dist/pool/result_pool.d.ts.map +1 -1
  109. package/dist/pool/result_pool.js +9 -6
  110. package/dist/pool/result_pool.js.map +1 -1
  111. package/package.json +17 -17
  112. package/src/js_render/computable_context.ts +37 -7
  113. package/src/js_render/context.ts +12 -5
  114. package/src/js_render/index.ts +1 -1
  115. package/src/middle_layer/block.ts +13 -14
  116. package/src/middle_layer/block_ctx.ts +70 -23
  117. package/src/middle_layer/block_ctx_unsafe.ts +11 -4
  118. package/src/middle_layer/middle_layer.ts +2 -1
  119. package/src/middle_layer/project.ts +86 -40
  120. package/src/middle_layer/project_overview.ts +44 -20
  121. package/src/middle_layer/render.test.ts +1 -1
  122. package/src/middle_layer/render.ts +1 -1
  123. package/src/model/block_storage_helper.ts +213 -0
  124. package/src/model/index.ts +2 -1
  125. package/src/model/project_helper.ts +249 -1
  126. package/src/model/project_model.ts +9 -5
  127. package/src/mutator/block-pack/block_pack.ts +1 -2
  128. package/src/mutator/migration.ts +79 -6
  129. package/src/mutator/project-v3.test.ts +280 -0
  130. package/src/mutator/project.test.ts +27 -27
  131. package/src/mutator/project.ts +351 -68
  132. package/src/pool/result_pool.ts +11 -4
@@ -1,4 +1,5 @@
1
- import { extractCodeWithInfo, type BlockConfig, type PlRef } from '@platforma-sdk/model';
1
+ import type { ResultOrError, BlockConfig, PlRef, ConfigRenderLambda } from '@platforma-sdk/model';
2
+ import { extractCodeWithInfo, ensureError } from '@platforma-sdk/model';
2
3
  import { LRUCache } from 'lru-cache';
3
4
  import type { QuickJSWASMModule } from 'quickjs-emscripten';
4
5
  import { executeSingleLambda } from '../js_render';
@@ -13,6 +14,44 @@ type EnrichmentTargetsValue = {
13
14
  value: PlRef[] | undefined;
14
15
  };
15
16
 
17
+ /**
18
+ * Result of VM-based storage normalization
19
+ */
20
+ interface NormalizeStorageResult {
21
+ storage: unknown;
22
+ data: unknown;
23
+ }
24
+
25
+ /**
26
+ * Result of VM-based storage migration.
27
+ * Returned by migrateStorageInVM().
28
+ *
29
+ * - Error result: { error: string } - serious failure (no context, etc.)
30
+ * - Success result: { newStorageJson: string, info: string, warn?: string } - migration succeeded or reset to initial
31
+ */
32
+ export type MigrationResult =
33
+ | { error: string }
34
+ | { error?: undefined; newStorageJson: string; info: string; warn?: string };
35
+
36
+ // Internal lambda handles for storage operations (registered by SDK's block_storage_vm.ts)
37
+ // All callbacks are prefixed with `__pl_` to indicate internal SDK use
38
+ const STORAGE_NORMALIZE_HANDLE: ConfigRenderLambda = { __renderLambda: true, handle: '__pl_storage_normalize' };
39
+ const STORAGE_APPLY_UPDATE_HANDLE: ConfigRenderLambda = { __renderLambda: true, handle: '__pl_storage_applyUpdate' };
40
+ const STORAGE_GET_INFO_HANDLE: ConfigRenderLambda = { __renderLambda: true, handle: '__pl_storage_getInfo' };
41
+ const STORAGE_MIGRATE_HANDLE: ConfigRenderLambda = { __renderLambda: true, handle: '__pl_storage_migrate' };
42
+ const ARGS_DERIVE_HANDLE: ConfigRenderLambda = { __renderLambda: true, handle: '__pl_args_derive' };
43
+ const PRERUN_ARGS_DERIVE_HANDLE: ConfigRenderLambda = { __renderLambda: true, handle: '__pl_prerunArgs_derive' };
44
+ // Registered by DataModel.registerCallbacks()
45
+ const INITIAL_STORAGE_HANDLE: ConfigRenderLambda = { __renderLambda: true, handle: '__pl_storage_initial' };
46
+
47
+ /**
48
+ * Result of args derivation from storage.
49
+ * Returned by __pl_args_derive and __pl_prerunArgs_derive VM callbacks.
50
+ */
51
+ type ArgsDeriveResult =
52
+ | { error: string }
53
+ | { error?: undefined; value: unknown };
54
+
16
55
  export class ProjectHelper {
17
56
  private readonly enrichmentTargetsCache = new LRUCache<string, EnrichmentTargetsValue, EnrichmentTargetsRequest>({
18
57
  max: 256,
@@ -23,6 +62,75 @@ export class ProjectHelper {
23
62
 
24
63
  constructor(private readonly quickJs: QuickJSWASMModule) {}
25
64
 
65
+ // =============================================================================
66
+ // Args Derivation from Storage (V3+)
67
+ // =============================================================================
68
+
69
+ /**
70
+ * Derives args directly from storage JSON using VM callback.
71
+ * The VM extracts data from storage and calls the block's args() function.
72
+ *
73
+ * This allows the middle layer to work only with storage JSON,
74
+ * without needing to know the underlying data structure.
75
+ *
76
+ * @param blockConfig The block configuration (provides the model code)
77
+ * @param storageJson Storage as JSON string
78
+ * @returns The derived args object, or error if derivation fails
79
+ */
80
+ public deriveArgsFromStorage(blockConfig: BlockConfig, storageJson: string): ResultOrError<unknown> {
81
+ if (blockConfig.modelAPIVersion !== 2) {
82
+ return { error: new Error('deriveArgsFromStorage is only supported for model API version 2') };
83
+ }
84
+
85
+ try {
86
+ const result = executeSingleLambda(
87
+ this.quickJs,
88
+ ARGS_DERIVE_HANDLE,
89
+ extractCodeWithInfo(blockConfig),
90
+ storageJson,
91
+ ) as ArgsDeriveResult;
92
+
93
+ if (result.error !== undefined) {
94
+ return { error: new Error(result.error) };
95
+ }
96
+ return { value: result.value };
97
+ } catch (e) {
98
+ return { error: new Error('Args derivation from storage failed', { cause: ensureError(e) }) };
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Derives prerunArgs directly from storage JSON using VM callback.
104
+ * Falls back to args() if prerunArgs is not defined in the block model.
105
+ *
106
+ * @param blockConfig The block configuration (provides the model code)
107
+ * @param storageJson Storage as JSON string
108
+ * @returns The derived prerunArgs, or undefined if derivation fails
109
+ */
110
+ public derivePrerunArgsFromStorage(blockConfig: BlockConfig, storageJson: string): unknown {
111
+ if (blockConfig.modelAPIVersion !== 2) {
112
+ throw new Error('derivePrerunArgsFromStorage is only supported for model API version 2');
113
+ }
114
+
115
+ try {
116
+ const result = executeSingleLambda(
117
+ this.quickJs,
118
+ PRERUN_ARGS_DERIVE_HANDLE,
119
+ extractCodeWithInfo(blockConfig),
120
+ storageJson,
121
+ ) as ArgsDeriveResult;
122
+
123
+ if (result.error !== undefined) {
124
+ // Return undefined if derivation fails (skip block in staging)
125
+ return undefined;
126
+ }
127
+ return result.value;
128
+ } catch {
129
+ // Return undefined if derivation fails (skip block in staging)
130
+ return undefined;
131
+ }
132
+ }
133
+
26
134
  private calculateEnrichmentTargets(req: EnrichmentTargetsRequest): PlRef[] | undefined {
27
135
  const blockConfig = req.blockConfig();
28
136
  if (blockConfig.enrichmentTargets === undefined) return undefined;
@@ -38,4 +146,144 @@ export class ProjectHelper {
38
146
  const cacheKey = `${key.argsRid}:${key.blockPackRid}`;
39
147
  return this.enrichmentTargetsCache.memo(cacheKey, { context: req }).value;
40
148
  }
149
+
150
+ // =============================================================================
151
+ // VM-based Storage Operations
152
+ // =============================================================================
153
+
154
+ /**
155
+ * Normalizes raw blockStorage data using VM-based transformation.
156
+ * This calls the model's `__pl_storage_normalize` callback which:
157
+ * - Handles BlockStorage format (with discriminator)
158
+ * - Handles legacy V1/V2 format ({ args, uiState })
159
+ * - Handles raw V3 state
160
+ *
161
+ * @param blockConfig The block configuration (provides the model code)
162
+ * @param rawStorage Raw storage data from resource tree (may be JSON string or object)
163
+ * @returns Object with { storage, state } or undefined if transformation fails
164
+ */
165
+ public normalizeStorageInVM(blockConfig: BlockConfig, rawStorage: unknown): NormalizeStorageResult | undefined {
166
+ try {
167
+ const result = executeSingleLambda(
168
+ this.quickJs,
169
+ STORAGE_NORMALIZE_HANDLE,
170
+ extractCodeWithInfo(blockConfig),
171
+ rawStorage,
172
+ ) as NormalizeStorageResult;
173
+ return result;
174
+ } catch (e) {
175
+ console.warn('[ProjectHelper.normalizeStorageInVM] Storage normalization failed:', e);
176
+ return undefined;
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Creates initial BlockStorage for a new block using VM-based transformation.
182
+ * This calls the '__pl_storage_initial' callback registered by DataModel which:
183
+ * - Gets initial data from DataModel.getDefaultData()
184
+ * - Creates BlockStorage with correct version
185
+ *
186
+ * @param blockConfig The block configuration (provides the model code)
187
+ * @returns Initial storage as JSON string
188
+ * @throws Error if storage creation fails
189
+ */
190
+ public getInitialStorageInVM(blockConfig: BlockConfig): string {
191
+ try {
192
+ const result = executeSingleLambda(
193
+ this.quickJs,
194
+ INITIAL_STORAGE_HANDLE,
195
+ extractCodeWithInfo(blockConfig),
196
+ ) as string;
197
+ return result;
198
+ } catch (e) {
199
+ console.error('[ProjectHelper.getInitialStorageInVM] Initial storage creation failed:', e);
200
+ throw new Error(`Block initial storage creation failed: ${e}`);
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Applies a state update using VM-based transformation.
206
+ * This calls the model's `__pl_storage_applyUpdate` callback which:
207
+ * - Normalizes current storage
208
+ * - Updates state while preserving other fields (version, plugins)
209
+ * - Returns the updated storage as JSON string
210
+ *
211
+ * @param blockConfig The block configuration (provides the model code)
212
+ * @param currentStorageJson Current storage as JSON string (must be defined)
213
+ * @param newState New state from developer
214
+ * @returns Updated storage as JSON string
215
+ * @throws Error if storage update fails
216
+ */
217
+ public applyStorageUpdateInVM(blockConfig: BlockConfig, currentStorageJson: string, payload: { operation: string; value: unknown }): string {
218
+ try {
219
+ const result = executeSingleLambda(
220
+ this.quickJs,
221
+ STORAGE_APPLY_UPDATE_HANDLE,
222
+ extractCodeWithInfo(blockConfig),
223
+ currentStorageJson,
224
+ payload,
225
+ ) as string;
226
+ return result;
227
+ } catch (e) {
228
+ console.error('[ProjectHelper.applyStorageUpdateInVM] Storage update failed:', e);
229
+ throw new Error(`Block storage update failed: ${e}`);
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Gets storage info from raw storage data by calling the VM's __pl_storage_getInfo callback.
235
+ * Returns structured info about the storage (e.g., dataVersion).
236
+ *
237
+ * @param blockConfig Block configuration
238
+ * @param rawStorageJson Raw storage as JSON string (or undefined)
239
+ * @returns Storage info as JSON string (e.g., '{"dataVersion": 1}')
240
+ */
241
+ public getStorageInfoInVM(blockConfig: BlockConfig, rawStorageJson: string | undefined): string | undefined {
242
+ try {
243
+ const result = executeSingleLambda(
244
+ this.quickJs,
245
+ STORAGE_GET_INFO_HANDLE,
246
+ extractCodeWithInfo(blockConfig),
247
+ rawStorageJson,
248
+ ) as string;
249
+ return result;
250
+ } catch (e) {
251
+ console.error('[ProjectHelper.getStorageInfoInVM] Get storage info failed:', e);
252
+ return undefined;
253
+ }
254
+ }
255
+
256
+ // =============================================================================
257
+ // Block State Migrations
258
+ // =============================================================================
259
+
260
+ /**
261
+ * Runs block state migrations via VM-based transformation.
262
+ * This calls the model's `__pl_storage_migrate` callback which:
263
+ * - Normalizes current storage to get state and version
264
+ * - Calculates target version from number of registered migrations
265
+ * - Runs all necessary migrations sequentially
266
+ * - Returns new storage with updated state and version
267
+ *
268
+ * The middle layer doesn't need to know about dataVersion or storage internals.
269
+ * All migration logic is encapsulated in the model.
270
+ *
271
+ * @param blockConfig The NEW block configuration (provides the model code with migrations)
272
+ * @param currentStorageJson Current storage as JSON string (or undefined)
273
+ * @returns MigrationResult with new storage or skip/error info
274
+ */
275
+ public migrateStorageInVM(blockConfig: BlockConfig, currentStorageJson: string | undefined): MigrationResult {
276
+ try {
277
+ const result = executeSingleLambda(
278
+ this.quickJs,
279
+ STORAGE_MIGRATE_HANDLE,
280
+ extractCodeWithInfo(blockConfig),
281
+ currentStorageJson,
282
+ ) as MigrationResult;
283
+ return result;
284
+ } catch (e) {
285
+ console.error('[ProjectHelper.migrateStorageInVM] Migration failed:', e);
286
+ return { error: `VM execution failed: ${e}` };
287
+ }
288
+ }
41
289
  }
@@ -73,7 +73,8 @@ export const InitialBlockMeta: ProjectMeta = {
73
73
  export const ProjectResourceType: ResourceType = { name: 'UserProject', version: '2' };
74
74
 
75
75
  export const SchemaVersionKey = 'SchemaVersion';
76
- export const SchemaVersionCurrent = '2';
76
+ export const SchemaVersionV2 = '2';
77
+ export const SchemaVersionCurrent = '3';
77
78
 
78
79
  export const ProjectCreatedTimestamp = 'ProjectCreated';
79
80
  export const ProjectLastModifiedTimestamp = 'ProjectLastModified';
@@ -101,9 +102,10 @@ export interface ProjectField {
101
102
  fieldName:
102
103
  | 'blockPack'
103
104
  | 'blockSettings'
104
- | 'uiState'
105
+ | 'blockStorage' // Persistent storage for v3 blocks (state, plugins data, etc.)
105
106
  | 'prodArgs'
106
107
  | 'currentArgs'
108
+ | 'currentPrerunArgs' // Derived args for staging/prerun rendering (from prerunArgs() or args())
107
109
  | 'prodCtx'
108
110
  | 'prodUiCtx'
109
111
  | 'prodOutput'
@@ -115,15 +117,17 @@ export interface ProjectField {
115
117
  | 'stagingOutput'
116
118
  | 'stagingCtxPrevious'
117
119
  | 'stagingUiCtxPrevious'
118
- | 'stagingOutputPrevious';
120
+ | 'stagingOutputPrevious'
121
+ ;
119
122
  }
120
123
 
121
124
  export const FieldsToDuplicate: Set<ProjectField['fieldName']> = new Set([
122
125
  'blockPack',
123
126
  'blockSettings',
124
- 'uiState',
127
+ 'blockStorage',
125
128
  'prodArgs',
126
129
  'currentArgs',
130
+ 'currentPrerunArgs',
127
131
  'prodCtx',
128
132
  'prodUiCtx',
129
133
  'prodOutput',
@@ -134,7 +138,7 @@ export function projectFieldName(blockId: string, fieldName: ProjectField['field
134
138
  }
135
139
 
136
140
  const projectFieldPattern
137
- = /^(?<blockId>.*)-(?<fieldName>blockPack|blockSettings|uiState|prodArgs|currentArgs|prodCtx|prodUiCtx|prodOutput|prodCtxPrevious|prodUiCtxPrevious|prodOutputPrevious|stagingCtx|stagingUiCtx|stagingOutput|stagingCtxPrevious|stagingUiCtxPrevious|stagingOutputPrevious)$/;
141
+ = /^(?<blockId>.*)-(?<fieldName>blockPack|blockSettings|blockStorage|inputsValid|prodArgs|currentArgs|currentPrerunArgs|prodCtx|prodUiCtx|prodOutput|prodCtxPrevious|prodUiCtxPrevious|prodOutputPrevious|stagingCtx|stagingUiCtx|stagingOutput|stagingCtxPrevious|stagingUiCtxPrevious|stagingOutputPrevious)$/;
138
142
 
139
143
  export function parseProjectField(name: string): ProjectField | undefined {
140
144
  const match = name.match(projectFieldPattern);
@@ -38,7 +38,7 @@ function parseStringConfig(configContent: string): BlockConfigContainer {
38
38
  }
39
39
 
40
40
  if (!Code.safeParse(res.data.code).success) {
41
- throw new Error('No code bundle');
41
+ throw new Error('parseStringConfig:No code bundle');
42
42
  }
43
43
 
44
44
  return res.data as BlockConfigContainer;
@@ -75,7 +75,6 @@ export class BlockPackPreparer {
75
75
 
76
76
  case 'dev-v1': {
77
77
  const devPaths = await resolveDevPacket(spec.folder, false);
78
- console.log('devPaths', devPaths);
79
78
  const configContent = await fs.promises.readFile(devPaths.config, { encoding: 'utf-8' });
80
79
  return JSON.parse(configContent);
81
80
  }
@@ -1,8 +1,8 @@
1
1
  import type { PlClient, PlTransaction, ResourceId } from '@milaboratories/pl-client';
2
- import type { ProjectStructure } from '../model/project_model';
3
- import { projectFieldName, ProjectStructureKey, SchemaVersionCurrent, SchemaVersionKey } from '../model/project_model';
2
+ import type { ProjectField, ProjectStructure } from '../model/project_model';
3
+ import { projectFieldName, ProjectStructureKey, SchemaVersionCurrent, SchemaVersionKey, SchemaVersionV2 } from '../model/project_model';
4
4
  import { BlockFrontendStateKeyPrefixV1, SchemaVersionV1 } from '../model/project_model_v1';
5
- import { field } from '@milaboratories/pl-client';
5
+ import { field, isNullResourceId } from '@milaboratories/pl-client';
6
6
  import { cachedDeserialize } from '@milaboratories/ts-helpers';
7
7
  import { allBlocks } from '../model/project_model_util';
8
8
 
@@ -14,13 +14,22 @@ import { allBlocks } from '../model/project_model_util';
14
14
  */
15
15
  export async function applyProjectMigrations(pl: PlClient, rid: ResourceId) {
16
16
  await pl.withWriteTx('ProjectMigration', async (tx) => {
17
- const schemaVersion = await tx.getKValueJson<string>(rid, SchemaVersionKey);
17
+ let schemaVersion = await tx.getKValueJson<string>(rid, SchemaVersionKey);
18
18
  if (schemaVersion === SchemaVersionCurrent) return;
19
+
20
+ // Apply migrations in sequence
19
21
  if (schemaVersion === SchemaVersionV1) {
20
22
  await migrateV1ToV2(tx, rid);
21
- } else {
23
+ schemaVersion = SchemaVersionV2;
24
+ }
25
+
26
+ if (schemaVersion === SchemaVersionV2) {
27
+ await migrateV2ToV3(tx, rid);
28
+ } else if (schemaVersion !== SchemaVersionV1) {
29
+ // If we got here and it's not v1 (which was handled above), it's unknown
22
30
  throw new Error(`Unknown project schema version: ${schemaVersion}`);
23
31
  }
32
+
24
33
  tx.setKValue(rid, SchemaVersionKey, JSON.stringify(SchemaVersionCurrent));
25
34
  await tx.commit();
26
35
  });
@@ -46,8 +55,72 @@ async function migrateV1ToV2(tx: PlTransaction, rid: ResourceId) {
46
55
  const uiState = kvMap.get(kvKey);
47
56
  const valueJson = uiState ? cachedDeserialize(uiState) : {};
48
57
  const uiStateR = tx.createJsonGzValue(valueJson);
49
- const uiStateF = field(rid, projectFieldName(block.id, 'uiState'));
58
+ const uiStateF = field(rid, projectFieldName(block.id, 'blockStorage'));
50
59
  tx.createField(uiStateF, 'Dynamic', uiStateR);
51
60
  tx.deleteKValue(rid, kvKey);
52
61
  }
53
62
  }
63
+
64
+ /**
65
+ * Migrates the project from schema version 2 to 3.
66
+ *
67
+ * Summary of changes:
68
+ * - Introduces unified 'blockStorage' field containing { args, uiState }
69
+ * - Adds 'currentPrerunArgs' field for staging/prerun rendering
70
+ * - For each block:
71
+ * 1. Read existing 'blockStorage' field (contains uiState in v2)
72
+ * 2. Read existing 'currentArgs' field (contains args)
73
+ * 3. Create unified state = { args: currentArgs, uiState: oldState }
74
+ * 4. Write to new {blockId}-blockStorage field (overwrites)
75
+ * 5. Initialize {blockId}-currentPrerunArgs (same as prodArgs for v1/v2 blocks)
76
+ * - Note: currentArgs and prodArgs fields remain for compatibility layer
77
+ *
78
+ * @param tx - The transaction to use.
79
+ * @param rid - The resource id of the project.
80
+ */
81
+ async function migrateV2ToV3(tx: PlTransaction, rid: ResourceId) {
82
+ const [structure, fullResourceState] = await Promise.all([
83
+ tx.getKValueJson<ProjectStructure>(rid, ProjectStructureKey),
84
+ tx.getResourceData(rid, true),
85
+ ]);
86
+
87
+ // Build a map of field name -> resource id for quick lookup
88
+ const fieldMap = new Map<string, ResourceId>();
89
+ for (const f of fullResourceState.fields) {
90
+ if (!isNullResourceId(f.value)) {
91
+ fieldMap.set(f.name, f.value);
92
+ }
93
+ }
94
+
95
+ for (const block of allBlocks(structure)) {
96
+ // Read existing field values
97
+ const uiStateFieldName = projectFieldName(block.id, 'uiState' as ProjectField['fieldName']);
98
+ const currentArgsFieldName = projectFieldName(block.id, 'currentArgs');
99
+
100
+ const uiStateRid = fieldMap.get(uiStateFieldName);
101
+ const currentArgsRid = fieldMap.get(currentArgsFieldName);
102
+
103
+ // Read field data in parallel where available
104
+ const [uiStateData, currentArgsData] = await Promise.all([
105
+ uiStateRid ? tx.getResourceData(uiStateRid, false) : Promise.resolve(undefined),
106
+ currentArgsRid ? tx.getResourceData(currentArgsRid, false) : Promise.resolve(undefined),
107
+ ]);
108
+
109
+ // Extract values - in v2, 'blockStorage' contains raw uiState, not wrapped
110
+ const uiState = uiStateData?.data ? cachedDeserialize(uiStateData.data) : {};
111
+ const args = currentArgsData?.data ? cachedDeserialize(currentArgsData.data) : {};
112
+
113
+ // Create unified state: { args, uiState }
114
+ const unifiedState = {
115
+ args,
116
+ uiState,
117
+ };
118
+
119
+ const blockStorageFieldName = projectFieldName(block.id, 'blockStorage');
120
+
121
+ // Write new unified blockStorage field (overwrite existing)
122
+ const stateR = tx.createJsonGzValue(unifiedState);
123
+ const stateF = field(rid, blockStorageFieldName);
124
+ tx.createField(stateF, 'Dynamic', stateR);
125
+ }
126
+ }