@milaboratories/milaboratories.pool-explorer.model 1.0.126 → 1.1.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.
package/dist/bundle.js CHANGED
@@ -4,14 +4,84 @@
4
4
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["block-model"] = {}));
5
5
  })(this, (function (exports) { 'use strict';
6
6
 
7
- //
8
- // Helpers
9
- //
10
- //
11
- // Json
12
- //
13
- function getImmediate(value) {
14
- return { type: 'Immediate', value };
7
+ /**
8
+ * BlockStorage - Typed storage abstraction for block persistent data.
9
+ *
10
+ * This module provides:
11
+ * - A typed structure for block storage with versioning and plugin support
12
+ * - Utility functions for manipulating storage
13
+ * - Handler interfaces for model-level customization
14
+ *
15
+ * @module block_storage
16
+ */
17
+ // =============================================================================
18
+ // Core Types
19
+ // =============================================================================
20
+ /**
21
+ * Discriminator key for BlockStorage format detection.
22
+ * This unique hash-based key identifies data as BlockStorage vs legacy formats.
23
+ */
24
+ const BLOCK_STORAGE_KEY = '__pl_a7f3e2b9__';
25
+ /**
26
+ * Current BlockStorage schema version.
27
+ * Increment this when the storage structure itself changes (not block state migrations).
28
+ */
29
+ const BLOCK_STORAGE_SCHEMA_VERSION = 'v1';
30
+ /**
31
+ * Type guard to check if a value is a valid BlockStorage object.
32
+ * Checks for the discriminator key and valid schema version.
33
+ */
34
+ function isBlockStorage(value) {
35
+ if (value === null || typeof value !== 'object')
36
+ return false;
37
+ const obj = value;
38
+ const schemaVersion = obj[BLOCK_STORAGE_KEY];
39
+ // Currently only 'v1' is valid, but this allows future versions
40
+ return schemaVersion === 'v1'; // Add more versions as schema evolves
41
+ }
42
+ // =============================================================================
43
+ // Factory Functions
44
+ // =============================================================================
45
+ /**
46
+ * Creates a BlockStorage with the given initial data
47
+ *
48
+ * @param initialData - The initial data value (defaults to empty object)
49
+ * @param version - The initial data version (defaults to 1)
50
+ * @returns A new BlockStorage instance with discriminator key
51
+ */
52
+ function createBlockStorage(initialData = {}, version = 1) {
53
+ return {
54
+ [BLOCK_STORAGE_KEY]: BLOCK_STORAGE_SCHEMA_VERSION,
55
+ __dataVersion: version,
56
+ __data: initialData,
57
+ };
58
+ }
59
+ // =============================================================================
60
+ // Data Access & Update Functions
61
+ // =============================================================================
62
+ /**
63
+ * Gets the data from BlockStorage
64
+ *
65
+ * @param storage - The BlockStorage instance
66
+ * @returns The data value
67
+ */
68
+ function getStorageData(storage) {
69
+ return storage.__data;
70
+ }
71
+ /**
72
+ * Updates the data in BlockStorage (immutable)
73
+ *
74
+ * @param storage - The current BlockStorage
75
+ * @param payload - The update payload with operation and value
76
+ * @returns A new BlockStorage with updated data
77
+ */
78
+ function updateStorageData(storage, payload) {
79
+ switch (payload.operation) {
80
+ case 'update-data':
81
+ return { ...storage, __data: payload.value };
82
+ default:
83
+ throw new Error(`Unknown storage operation: ${payload.operation}`);
84
+ }
15
85
  }
16
86
 
17
87
  /** Utility code helping to identify whether the code is running in actual UI environment */
@@ -48,6 +118,41 @@
48
118
  ctx.callbackRegistry[key] = callback;
49
119
  return true;
50
120
  }
121
+ /**
122
+ * Registers a callback, replacing any existing callback with the same key.
123
+ * Use this for callbacks that have a default value but can be overridden.
124
+ *
125
+ * @param key - The callback registry key
126
+ * @param callback - The callback function to register
127
+ * @returns true if registered, false if not in render context
128
+ */
129
+ function replaceCallback(key, callback) {
130
+ const ctx = tryGetCfgRenderCtx();
131
+ if (ctx === undefined)
132
+ return false;
133
+ ctx.callbackRegistry[key] = callback;
134
+ return true;
135
+ }
136
+ /** Creates a ConfigRenderLambda descriptor without registering a callback. */
137
+ function createRenderLambda(opts) {
138
+ const { handle, ...flags } = opts;
139
+ return {
140
+ __renderLambda: true,
141
+ handle,
142
+ ...flags,
143
+ };
144
+ }
145
+ /** Registers a callback and returns a ConfigRenderLambda descriptor. */
146
+ function createAndRegisterRenderLambda(opts, replace) {
147
+ const { handle, lambda, ...flags } = opts;
148
+ if (replace) {
149
+ replaceCallback(handle, lambda);
150
+ }
151
+ else {
152
+ tryRegisterCallback(handle, lambda);
153
+ }
154
+ return createRenderLambda({ handle, ...flags });
155
+ }
51
156
  const futureResolves = new Map();
52
157
  function registerFutureAwait(handle, onResolve) {
53
158
  if (!(handle in getCfgRenderCtx().callbackRegistry)) {
@@ -7253,28 +7358,19 @@
7253
7358
  }
7254
7359
  }
7255
7360
  /** Main entry point to the API available within model lambdas (like outputs, sections, etc..) */
7256
- class RenderCtx {
7361
+ class RenderCtxBase {
7257
7362
  ctx;
7258
7363
  constructor() {
7259
7364
  this.ctx = getCfgRenderCtx();
7260
7365
  }
7261
- _argsCache;
7262
- get args() {
7263
- if (this._argsCache === undefined) {
7264
- const raw = this.ctx.args;
7265
- const value = typeof raw === 'function' ? raw() : raw;
7266
- this._argsCache = { v: JSON.parse(value) };
7267
- }
7268
- return this._argsCache.v;
7269
- }
7270
- _uiStateCache;
7271
- get uiState() {
7272
- if (this._uiStateCache === undefined) {
7273
- const raw = this.ctx.uiState;
7366
+ _dataCache;
7367
+ get data() {
7368
+ if (this._dataCache === undefined) {
7369
+ const raw = this.ctx.data;
7274
7370
  const value = typeof raw === 'function' ? raw() : raw;
7275
- this._uiStateCache = { v: value ? JSON.parse(value) : {} };
7371
+ this._dataCache = { v: value ? JSON.parse(value) : {} };
7276
7372
  }
7277
- return this._uiStateCache.v;
7373
+ return this._dataCache.v;
7278
7374
  }
7279
7375
  // lazy rendering because this feature is rarely used
7280
7376
  _activeArgsCache;
@@ -7385,8 +7481,21 @@
7385
7481
  this.ctx.logError(msg);
7386
7482
  }
7387
7483
  }
7484
+ /** Main entry point to the API available within model lambdas (like outputs, sections, etc..) for v3+ blocks */
7485
+ class RenderCtx extends RenderCtxBase {
7486
+ _argsCache;
7487
+ get args() {
7488
+ if (this._argsCache === undefined) {
7489
+ const raw = this.ctx.args;
7490
+ const value = typeof raw === 'function' ? raw() : raw;
7491
+ // args can be undefined when derivation fails (e.g., validation error in args())
7492
+ this._argsCache = { v: value === undefined ? undefined : JSON.parse(value) };
7493
+ }
7494
+ return this._argsCache.v;
7495
+ }
7496
+ }
7388
7497
 
7389
- var version = "1.51.9";
7498
+ var version = "1.52.0";
7390
7499
 
7391
7500
  const PlatformaSDKVersion = version;
7392
7501
 
@@ -7402,56 +7511,315 @@
7402
7511
  return data;
7403
7512
  }
7404
7513
 
7514
+ /**
7515
+ * BlockStorage VM Integration - Internal module for VM-based storage operations.
7516
+ *
7517
+ * This module auto-registers internal callbacks that the middle layer can invoke
7518
+ * to perform storage transformations. Block developers never interact with these
7519
+ * directly - they only see `state`.
7520
+ *
7521
+ * Registered callbacks (all prefixed with `__pl_` for internal SDK use):
7522
+ * - `__pl_storage_normalize`: (rawStorage) => { storage, data }
7523
+ * - `__pl_storage_applyUpdate`: (currentStorageJson, payload) => updatedStorageJson
7524
+ * - `__pl_storage_getInfo`: (rawStorage) => JSON string with storage info
7525
+ * - `__pl_storage_migrate`: (currentStorageJson) => MigrationResult
7526
+ * - `__pl_args_derive`: (storageJson) => ArgsDeriveResult
7527
+ * - `__pl_prerunArgs_derive`: (storageJson) => ArgsDeriveResult
7528
+ *
7529
+ * Callbacks registered by DataModel.registerCallbacks():
7530
+ * - `__pl_data_initial`: () => initial data
7531
+ * - `__pl_data_upgrade`: (versioned) => UpgradeResult
7532
+ * - `__pl_storage_initial`: () => initial BlockStorage as JSON string
7533
+ *
7534
+ * @module block_storage_vm
7535
+ * @internal
7536
+ */
7537
+ /**
7538
+ * Normalizes raw storage data and extracts state.
7539
+ * Handles all formats:
7540
+ * - New BlockStorage format (has discriminator)
7541
+ * - Legacy V1/V2 format ({ args, uiState })
7542
+ * - Raw V3 state (any other format)
7543
+ *
7544
+ * @param rawStorage - Raw data from blockStorage field (may be JSON string or object)
7545
+ * @returns Object with normalized storage and extracted state
7546
+ */
7547
+ function normalizeStorage(rawStorage) {
7548
+ // Handle undefined/null
7549
+ if (rawStorage === undefined || rawStorage === null) {
7550
+ const storage = createBlockStorage({});
7551
+ return { storage, data: {} };
7552
+ }
7553
+ // Parse JSON string if needed
7554
+ let parsed = rawStorage;
7555
+ if (typeof rawStorage === 'string') {
7556
+ try {
7557
+ parsed = JSON.parse(rawStorage);
7558
+ }
7559
+ catch {
7560
+ // If parsing fails, treat string as the data
7561
+ const storage = createBlockStorage(rawStorage);
7562
+ return { storage, data: rawStorage };
7563
+ }
7564
+ }
7565
+ // Check for BlockStorage format (has discriminator)
7566
+ if (isBlockStorage(parsed)) {
7567
+ return { storage: parsed, data: getStorageData(parsed) };
7568
+ }
7569
+ // Check for legacy V1/V2 format: { args, uiState }
7570
+ if (isLegacyModelV1ApiFormat(parsed)) {
7571
+ // For legacy format, the whole object IS the data
7572
+ const storage = createBlockStorage(parsed);
7573
+ return { storage, data: parsed };
7574
+ }
7575
+ // Raw V3 data - wrap it
7576
+ const storage = createBlockStorage(parsed);
7577
+ return { storage, data: parsed };
7578
+ }
7579
+ /**
7580
+ * Applies a state update to existing storage.
7581
+ * Used when setData is called from the frontend.
7582
+ *
7583
+ * @param currentStorageJson - Current storage as JSON string (must be defined)
7584
+ * @param newData - New data from application
7585
+ * @returns Updated storage as JSON string
7586
+ */
7587
+ function applyStorageUpdate(currentStorageJson, payload) {
7588
+ const { storage: currentStorage } = normalizeStorage(currentStorageJson);
7589
+ // Update data while preserving other storage fields (version, plugins)
7590
+ const updatedStorage = updateStorageData(currentStorage, payload);
7591
+ return JSON.stringify(updatedStorage);
7592
+ }
7593
+ /**
7594
+ * Checks if data is in legacy Model API v1 format.
7595
+ * Legacy format has { args, uiState? } at top level without the BlockStorage discriminator.
7596
+ */
7597
+ function isLegacyModelV1ApiFormat(data) {
7598
+ if (data === null || typeof data !== 'object')
7599
+ return false;
7600
+ if (isBlockStorage(data))
7601
+ return false;
7602
+ const obj = data;
7603
+ return 'args' in obj;
7604
+ }
7605
+ // =============================================================================
7606
+ // Auto-register internal callbacks when module is loaded in VM
7607
+ // =============================================================================
7608
+ // Register normalize callback
7609
+ tryRegisterCallback('__pl_storage_normalize', (rawStorage) => {
7610
+ return normalizeStorage(rawStorage);
7611
+ });
7612
+ // Register apply update callback (requires existing storage)
7613
+ tryRegisterCallback('__pl_storage_applyUpdate', (currentStorageJson, payload) => {
7614
+ return applyStorageUpdate(currentStorageJson, payload);
7615
+ });
7616
+ /**
7617
+ * Gets storage info from raw storage data.
7618
+ * Returns structured info about the storage state.
7619
+ *
7620
+ * @param rawStorage - Raw data from blockStorage field (may be JSON string or object)
7621
+ * @returns JSON string with storage info
7622
+ */
7623
+ function getStorageInfo(rawStorage) {
7624
+ const { storage } = normalizeStorage(rawStorage);
7625
+ const info = {
7626
+ dataVersion: storage.__dataVersion,
7627
+ };
7628
+ return JSON.stringify(info);
7629
+ }
7630
+ // Register get info callback
7631
+ tryRegisterCallback('__pl_storage_getInfo', (rawStorage) => {
7632
+ return getStorageInfo(rawStorage);
7633
+ });
7634
+ /**
7635
+ * Runs storage migration using the DataModel's upgrade callback.
7636
+ * This is the main entry point for the middle layer to trigger migrations.
7637
+ *
7638
+ * Uses the '__pl_data_upgrade' callback registered by DataModel.registerCallbacks() which:
7639
+ * - Handles all migration logic internally
7640
+ * - Returns { version, data, warning? } - warning present if reset to initial data
7641
+ *
7642
+ * @param currentStorageJson - Current storage as JSON string (or undefined)
7643
+ * @returns MigrationResult
7644
+ */
7645
+ function migrateStorage(currentStorageJson) {
7646
+ // Get the callback registry context
7647
+ const ctx = tryGetCfgRenderCtx();
7648
+ if (ctx === undefined) {
7649
+ return { error: 'Not in config rendering context' };
7650
+ }
7651
+ // Normalize storage to get current data and version
7652
+ const { storage: currentStorage, data: currentData } = normalizeStorage(currentStorageJson);
7653
+ const currentVersion = currentStorage.__dataVersion;
7654
+ // Helper to create storage with given data and version
7655
+ const createStorageJson = (data, version) => {
7656
+ return JSON.stringify({
7657
+ ...currentStorage,
7658
+ __dataVersion: version,
7659
+ __data: data,
7660
+ });
7661
+ };
7662
+ // Get the upgrade callback (registered by DataModel.registerCallbacks())
7663
+ const upgradeCallback = ctx.callbackRegistry['__pl_data_upgrade'];
7664
+ if (typeof upgradeCallback !== 'function') {
7665
+ return { error: '__pl_data_upgrade callback not found (DataModel not registered)' };
7666
+ }
7667
+ // Call the migrator's upgrade function
7668
+ let result;
7669
+ try {
7670
+ result = upgradeCallback({ version: currentVersion, data: currentData });
7671
+ }
7672
+ catch (e) {
7673
+ const errorMsg = e instanceof Error ? e.message : String(e);
7674
+ return { error: `upgrade() threw: ${errorMsg}` };
7675
+ }
7676
+ // Build info message
7677
+ const info = result.version === currentVersion
7678
+ ? `No migration needed (v${currentVersion})`
7679
+ : result.warning
7680
+ ? `Reset to initial data (v${result.version})`
7681
+ : `Migrated v${currentVersion}→v${result.version}`;
7682
+ return {
7683
+ newStorageJson: createStorageJson(result.data, result.version),
7684
+ info,
7685
+ warn: result.warning,
7686
+ };
7687
+ }
7688
+ // Register migrate callback
7689
+ tryRegisterCallback('__pl_storage_migrate', (currentStorageJson) => {
7690
+ return migrateStorage(currentStorageJson);
7691
+ });
7692
+ /**
7693
+ * Derives args from storage using the registered 'args' callback.
7694
+ * This extracts data from storage and passes it to the block's args() function.
7695
+ *
7696
+ * @param storageJson - Storage as JSON string
7697
+ * @returns ArgsDeriveResult with derived args or error
7698
+ */
7699
+ function deriveArgsFromStorage(storageJson) {
7700
+ const ctx = tryGetCfgRenderCtx();
7701
+ if (ctx === undefined) {
7702
+ return { error: 'Not in config rendering context' };
7703
+ }
7704
+ // Extract data from storage
7705
+ const { data } = normalizeStorage(storageJson);
7706
+ // Get the args callback (registered by BlockModelV3.args())
7707
+ const argsCallback = ctx.callbackRegistry['args'];
7708
+ if (typeof argsCallback !== 'function') {
7709
+ return { error: 'args callback not found' };
7710
+ }
7711
+ // Call the args callback with extracted data
7712
+ try {
7713
+ const result = argsCallback(data);
7714
+ return { value: result };
7715
+ }
7716
+ catch (e) {
7717
+ const errorMsg = e instanceof Error ? e.message : String(e);
7718
+ return { error: `args() threw: ${errorMsg}` };
7719
+ }
7720
+ }
7721
+ // Register args derivation callback
7722
+ tryRegisterCallback('__pl_args_derive', (storageJson) => {
7723
+ return deriveArgsFromStorage(storageJson);
7724
+ });
7725
+ /**
7726
+ * Derives prerunArgs from storage using the registered 'prerunArgs' callback.
7727
+ * Falls back to 'args' callback if 'prerunArgs' is not defined.
7728
+ *
7729
+ * @param storageJson - Storage as JSON string
7730
+ * @returns ArgsDeriveResult with derived prerunArgs or error
7731
+ */
7732
+ function derivePrerunArgsFromStorage(storageJson) {
7733
+ const ctx = tryGetCfgRenderCtx();
7734
+ if (ctx === undefined) {
7735
+ return { error: 'Not in config rendering context' };
7736
+ }
7737
+ // Extract data from storage
7738
+ const { data } = normalizeStorage(storageJson);
7739
+ // Try prerunArgs callback first
7740
+ const prerunArgsCallback = ctx.callbackRegistry['prerunArgs'];
7741
+ if (typeof prerunArgsCallback === 'function') {
7742
+ try {
7743
+ const result = prerunArgsCallback(data);
7744
+ return { value: result };
7745
+ }
7746
+ catch (e) {
7747
+ const errorMsg = e instanceof Error ? e.message : String(e);
7748
+ return { error: `prerunArgs() threw: ${errorMsg}` };
7749
+ }
7750
+ }
7751
+ // Fall back to args callback
7752
+ const argsCallback = ctx.callbackRegistry['args'];
7753
+ if (typeof argsCallback !== 'function') {
7754
+ return { error: 'args callback not found (fallback from missing prerunArgs)' };
7755
+ }
7756
+ try {
7757
+ const result = argsCallback(data);
7758
+ return { value: result };
7759
+ }
7760
+ catch (e) {
7761
+ const errorMsg = e instanceof Error ? e.message : String(e);
7762
+ return { error: `args() threw (fallback): ${errorMsg}` };
7763
+ }
7764
+ }
7765
+ // Register prerunArgs derivation callback
7766
+ tryRegisterCallback('__pl_prerunArgs_derive', (storageJson) => {
7767
+ return derivePrerunArgsFromStorage(storageJson);
7768
+ });
7769
+
7405
7770
  /** Main entry point that each block should use in it's "config" module. Don't forget
7406
7771
  * to call {@link done()} at the end of configuration. Value returned by this builder must be
7407
- * exported as constant with name "platforma" from the "config" module. */
7408
- class BlockModel {
7772
+ * exported as constant with name "platforma" from the "config" module.
7773
+ * API version is 3 (for UI) and 2 (for model) */
7774
+ class BlockModelV3 {
7409
7775
  config;
7410
7776
  constructor(config) {
7411
7777
  this.config = config;
7412
7778
  }
7413
- static get INITIAL_BLOCK_FEATURE_FLAGS() {
7414
- return {
7415
- supportsLazyState: true,
7416
- requiresUIAPIVersion: 1,
7417
- requiresModelAPIVersion: 1,
7418
- };
7419
- }
7420
- static create(renderingMode = 'Heavy') {
7421
- return new BlockModel({
7779
+ static INITIAL_BLOCK_FEATURE_FLAGS = {
7780
+ supportsLazyState: true,
7781
+ requiresUIAPIVersion: 3,
7782
+ requiresModelAPIVersion: 2,
7783
+ };
7784
+ /**
7785
+ * Creates a new BlockModelV3 builder with the specified data model and options.
7786
+ *
7787
+ * @example
7788
+ * const dataModel = DataModel.create<BlockData>(() => ({ numbers: [], labels: [] }));
7789
+ * BlockModelV3.create({ dataModel })
7790
+ * .args((data) => ({ numbers: data.numbers }))
7791
+ * .sections(() => [{ type: 'link', href: '/', label: 'Main' }])
7792
+ * .done();
7793
+ *
7794
+ * @param options Configuration options including required data model
7795
+ */
7796
+ static create(options) {
7797
+ const { dataModel, renderingMode = 'Heavy' } = options;
7798
+ // Register data model callbacks for VM use (initialData and upgrade)
7799
+ dataModel.registerCallbacks();
7800
+ return new BlockModelV3({
7422
7801
  renderingMode,
7423
- initialUiState: {},
7802
+ dataModel,
7424
7803
  outputs: {},
7425
- inputsValid: getImmediate(true),
7426
- sections: getImmediate([]),
7427
- featureFlags: BlockModel.INITIAL_BLOCK_FEATURE_FLAGS,
7804
+ // Register default sections callback (returns empty array)
7805
+ sections: createAndRegisterRenderLambda({ handle: 'sections', lambda: () => [] }, true),
7806
+ title: undefined,
7807
+ subtitle: undefined,
7808
+ tags: undefined,
7809
+ enrichmentTargets: undefined,
7810
+ featureFlags: { ...BlockModelV3.INITIAL_BLOCK_FEATURE_FLAGS },
7811
+ args: undefined,
7812
+ prerunArgs: undefined,
7428
7813
  });
7429
7814
  }
7430
7815
  output(key, cfgOrRf, flags = {}) {
7431
- if (typeof cfgOrRf === 'function') {
7432
- const handle = `output#${key}`;
7433
- tryRegisterCallback(handle, () => cfgOrRf(new RenderCtx()));
7434
- return new BlockModel({
7435
- ...this.config,
7436
- outputs: {
7437
- ...this.config.outputs,
7438
- [key]: {
7439
- __renderLambda: true,
7440
- handle,
7441
- ...flags,
7442
- },
7443
- },
7444
- });
7445
- }
7446
- else {
7447
- return new BlockModel({
7448
- ...this.config,
7449
- outputs: {
7450
- ...this.config.outputs,
7451
- [key]: cfgOrRf,
7452
- },
7453
- });
7454
- }
7816
+ return new BlockModelV3({
7817
+ ...this.config,
7818
+ outputs: {
7819
+ ...this.config.outputs,
7820
+ [key]: createAndRegisterRenderLambda({ handle: `output#${key}`, lambda: () => cfgOrRf(new RenderCtx()), ...flags }),
7821
+ },
7822
+ });
7455
7823
  }
7456
7824
  /** Shortcut for {@link output} with retentive flag set to true. */
7457
7825
  retentiveOutput(key, rf) {
@@ -7461,109 +7829,83 @@
7461
7829
  outputWithStatus(key, rf) {
7462
7830
  return this.output(key, rf, { withStatus: true });
7463
7831
  }
7464
- /** Shortcut for {@link output} with retentive and withStatus flags set to true. */
7465
- retentiveOutputWithStatus(key, rf) {
7466
- return this.output(key, rf, { retentive: true, withStatus: true });
7467
- }
7468
- argsValid(cfgOrRf) {
7469
- if (typeof cfgOrRf === 'function') {
7470
- tryRegisterCallback('inputsValid', () => cfgOrRf(new RenderCtx()));
7471
- return new BlockModel({
7472
- ...this.config,
7473
- inputsValid: {
7474
- __renderLambda: true,
7475
- handle: 'inputsValid',
7476
- },
7477
- });
7478
- }
7479
- else {
7480
- return new BlockModel({
7481
- ...this.config,
7482
- inputsValid: cfgOrRf,
7483
- });
7484
- }
7485
- }
7486
- sections(arrOrCfgOrRf) {
7487
- if (Array.isArray(arrOrCfgOrRf)) {
7488
- return this.sections(getImmediate(arrOrCfgOrRf));
7489
- }
7490
- else if (typeof arrOrCfgOrRf === 'function') {
7491
- tryRegisterCallback('sections', () => arrOrCfgOrRf(new RenderCtx()));
7492
- return new BlockModel({
7493
- ...this.config,
7494
- sections: {
7495
- __renderLambda: true,
7496
- handle: 'sections',
7497
- },
7498
- });
7499
- }
7500
- else {
7501
- return new BlockModel({
7502
- ...this.config,
7503
- sections: arrOrCfgOrRf,
7504
- });
7505
- }
7506
- }
7507
- /** Sets a rendering function to derive block title, shown for the block in the left blocks-overview panel. */
7508
- title(rf) {
7509
- tryRegisterCallback('title', () => rf(new RenderCtx()));
7510
- return new BlockModel({
7832
+ /**
7833
+ * Sets a function to derive block args from data.
7834
+ * This is called during setData to compute the args that will be used for block execution.
7835
+ *
7836
+ * @example
7837
+ * .args<BlockArgs>((data) => ({ numbers: data.numbers }))
7838
+ *
7839
+ * @example
7840
+ * .args<BlockArgs>((data) => {
7841
+ * if (data.numbers.length === 0) throw new Error('Numbers required'); // block not ready
7842
+ * return { numbers: data.numbers };
7843
+ * })
7844
+ */
7845
+ args(lambda) {
7846
+ return new BlockModelV3({
7511
7847
  ...this.config,
7512
- title: {
7513
- __renderLambda: true,
7514
- handle: 'title',
7515
- },
7848
+ args: createAndRegisterRenderLambda({ handle: 'args', lambda }),
7516
7849
  });
7517
7850
  }
7518
- subtitle(rf) {
7519
- tryRegisterCallback('subtitle', () => rf(new RenderCtx()));
7520
- return new BlockModel({
7851
+ /**
7852
+ * Sets a function to derive pre-run args from data (optional).
7853
+ * This is called during setData to compute the args that will be used for staging/pre-run phase.
7854
+ *
7855
+ * If not defined, defaults to using the args() function result.
7856
+ * If defined, uses its return value for the staging / prerun phase.
7857
+ *
7858
+ * The staging / prerun phase runs only if currentPrerunArgs differs from the executed
7859
+ * version of prerunArgs (same comparison logic as currentArgs vs prodArgs).
7860
+ *
7861
+ * @example
7862
+ * .prerunArgs((data) => ({ numbers: data.numbers }))
7863
+ *
7864
+ * @example
7865
+ * .prerunArgs((data) => {
7866
+ * // Return undefined to skip staging for this block
7867
+ * if (!data.isReady) return undefined;
7868
+ * return { numbers: data.numbers };
7869
+ * })
7870
+ */
7871
+ prerunArgs(fn) {
7872
+ return new BlockModelV3({
7521
7873
  ...this.config,
7522
- subtitle: {
7523
- __renderLambda: true,
7524
- handle: 'subtitle',
7525
- },
7874
+ prerunArgs: createAndRegisterRenderLambda({ handle: 'prerunArgs', lambda: fn }),
7526
7875
  });
7527
7876
  }
7528
- tags(rf) {
7529
- tryRegisterCallback('tags', () => rf(new RenderCtx()));
7530
- return new BlockModel({
7877
+ /** Sets the lambda to generate list of sections in the left block overviews panel. */
7878
+ sections(rf) {
7879
+ return new BlockModelV3({
7531
7880
  ...this.config,
7532
- tags: {
7533
- __renderLambda: true,
7534
- handle: 'tags',
7535
- },
7881
+ // Replace the default sections callback with the user-provided one
7882
+ sections: createAndRegisterRenderLambda({ handle: 'sections', lambda: () => rf(new RenderCtx()) }, true),
7536
7883
  });
7537
7884
  }
7538
- /**
7539
- * Sets initial args for the block, this value must be specified.
7540
- * @deprecated use {@link withArgs}
7541
- * */
7542
- initialArgs(value) {
7543
- return this.withArgs(value);
7885
+ /** Sets a rendering function to derive block title, shown for the block in the left blocks-overview panel. */
7886
+ title(rf) {
7887
+ return new BlockModelV3({
7888
+ ...this.config,
7889
+ title: createAndRegisterRenderLambda({ handle: 'title', lambda: () => rf(new RenderCtx()) }),
7890
+ });
7544
7891
  }
7545
- /** Sets initial args for the block, this value must be specified. */
7546
- withArgs(initialArgs) {
7547
- return new BlockModel({
7892
+ subtitle(rf) {
7893
+ return new BlockModelV3({
7548
7894
  ...this.config,
7549
- initialArgs,
7895
+ subtitle: createAndRegisterRenderLambda({ handle: 'subtitle', lambda: () => rf(new RenderCtx()) }),
7550
7896
  });
7551
7897
  }
7552
- /** Defines type and sets initial value for block UiState. */
7553
- withUiState(initialUiState) {
7554
- return new BlockModel({
7898
+ tags(rf) {
7899
+ return new BlockModelV3({
7555
7900
  ...this.config,
7556
- initialUiState,
7901
+ tags: createAndRegisterRenderLambda({ handle: 'tags', lambda: () => rf(new RenderCtx()) }),
7557
7902
  });
7558
7903
  }
7559
7904
  /** Sets or overrides feature flags for the block. */
7560
7905
  withFeatureFlags(flags) {
7561
- return new BlockModel({
7906
+ return new BlockModelV3({
7562
7907
  ...this.config,
7563
- featureFlags: {
7564
- ...this.config.featureFlags,
7565
- ...flags,
7566
- },
7908
+ featureFlags: { ...this.config.featureFlags, ...flags },
7567
7909
  });
7568
7910
  }
7569
7911
  /**
@@ -7571,58 +7913,58 @@
7571
7913
  * Influences dependency graph construction.
7572
7914
  */
7573
7915
  enriches(lambda) {
7574
- tryRegisterCallback('enrichmentTargets', lambda);
7575
- return new BlockModel({
7916
+ return new BlockModelV3({
7576
7917
  ...this.config,
7577
- enrichmentTargets: {
7578
- __renderLambda: true,
7579
- handle: 'enrichmentTargets',
7580
- },
7918
+ enrichmentTargets: createAndRegisterRenderLambda({ handle: 'enrichmentTargets', lambda: lambda }),
7581
7919
  });
7582
7920
  }
7583
7921
  /** Renders all provided block settings into a pre-configured platforma API
7584
- * instance, that can be used in frontend to interact with block state, and
7922
+ * instance, that can be used in frontend to interact with block data, and
7585
7923
  * other features provided by the platforma to the block. */
7586
- done(apiVersion = 1) {
7924
+ done() {
7587
7925
  return this.withFeatureFlags({
7588
7926
  ...this.config.featureFlags,
7589
- requiresUIAPIVersion: apiVersion,
7590
- }).#done();
7591
- }
7592
- #done() {
7593
- if (this.config.initialArgs === undefined)
7594
- throw new Error('Initial arguments not set.');
7595
- const config = {
7596
- v3: {
7927
+ })._done();
7928
+ }
7929
+ _done() {
7930
+ if (this.config.args === undefined)
7931
+ throw new Error('Args rendering function not set.');
7932
+ const apiVersion = 3;
7933
+ const migrationCount = this.config.dataModel.migrationCount;
7934
+ const blockConfig = {
7935
+ v4: {
7936
+ configVersion: 4,
7937
+ modelAPIVersion: 2,
7597
7938
  sdkVersion: PlatformaSDKVersion,
7598
7939
  renderingMode: this.config.renderingMode,
7599
- initialArgs: this.config.initialArgs,
7600
- initialUiState: this.config.initialUiState,
7601
- inputsValid: this.config.inputsValid,
7940
+ args: this.config.args,
7941
+ prerunArgs: this.config.prerunArgs,
7942
+ // Reference to __pl_data_initial callback registered by DataModel
7943
+ initialData: createRenderLambda({ handle: '__pl_data_initial' }),
7602
7944
  sections: this.config.sections,
7603
7945
  title: this.config.title,
7604
- subtitle: this.config.subtitle,
7605
- tags: this.config.tags,
7606
7946
  outputs: this.config.outputs,
7607
7947
  enrichmentTargets: this.config.enrichmentTargets,
7608
7948
  featureFlags: this.config.featureFlags,
7949
+ // Generate migration descriptors (indices for metadata)
7950
+ migrations: migrationCount > 0
7951
+ ? Array.from({ length: migrationCount }, (_, i) => ({ index: i }))
7952
+ : undefined,
7609
7953
  },
7610
7954
  // fields below are added to allow previous desktop versions read generated configs
7611
7955
  sdkVersion: PlatformaSDKVersion,
7612
7956
  renderingMode: this.config.renderingMode,
7613
- initialArgs: this.config.initialArgs,
7614
- inputsValid: downgradeCfgOrLambda(this.config.inputsValid),
7615
- sections: downgradeCfgOrLambda(this.config.sections),
7957
+ sections: this.config.sections,
7616
7958
  outputs: Object.fromEntries(Object.entries(this.config.outputs).map(([key, value]) => [key, downgradeCfgOrLambda(value)])),
7617
7959
  };
7618
- globalThis.platformaApiVersion = this.config.featureFlags.requiresUIAPIVersion;
7960
+ globalThis.platformaApiVersion = apiVersion;
7619
7961
  if (!isInUI())
7620
7962
  // we are in the configuration rendering routine, not in actual UI
7621
- return { config };
7963
+ return { config: blockConfig };
7622
7964
  // normal operation inside the UI
7623
7965
  else
7624
7966
  return {
7625
- ...getPlatformaInstance({ sdkVersion: PlatformaSDKVersion, apiVersion: platformaApiVersion }),
7967
+ ...getPlatformaInstance({ sdkVersion: PlatformaSDKVersion, apiVersion }),
7626
7968
  blockModelInfo: {
7627
7969
  outputs: Object.fromEntries(Object.entries(this.config.outputs)
7628
7970
  .map(([key, value]) => [key, {
@@ -7633,6 +7975,137 @@
7633
7975
  }
7634
7976
  }
7635
7977
 
7978
+ /** Internal builder for chaining migrations */
7979
+ class DataModelBuilder {
7980
+ migrationSteps;
7981
+ constructor(steps = []) {
7982
+ this.migrationSteps = steps;
7983
+ }
7984
+ /** Start a migration chain from an initial type */
7985
+ static from() {
7986
+ return new DataModelBuilder();
7987
+ }
7988
+ /** Add a migration step */
7989
+ migrate(fn) {
7990
+ return new DataModelBuilder([...this.migrationSteps, fn]);
7991
+ }
7992
+ /** Finalize with initial data, creating the DataModel */
7993
+ create(initialData, ..._) {
7994
+ return DataModel._fromBuilder(this.migrationSteps, initialData);
7995
+ }
7996
+ }
7997
+ /**
7998
+ * DataModel defines the block's data structure, initial values, and migrations.
7999
+ * Used by BlockModelV3 to manage data state.
8000
+ *
8001
+ * @example
8002
+ * // Simple data model (no migrations)
8003
+ * const dataModel = DataModel.create<BlockData>(() => ({
8004
+ * numbers: [],
8005
+ * labels: [],
8006
+ * }));
8007
+ *
8008
+ * // Data model with migrations
8009
+ * const dataModel = DataModel
8010
+ * .from<V1>()
8011
+ * .migrate((data) => ({ ...data, labels: [] })) // v1 → v2
8012
+ * .migrate((data) => ({ ...data, description: '' })) // v2 → v3
8013
+ * .create<BlockData>(() => ({ numbers: [], labels: [], description: '' }));
8014
+ */
8015
+ class DataModel {
8016
+ steps;
8017
+ _initialData;
8018
+ constructor(steps, initialData) {
8019
+ this.steps = steps;
8020
+ this._initialData = initialData;
8021
+ }
8022
+ /** Start a migration chain from an initial type */
8023
+ static from() {
8024
+ return DataModelBuilder.from();
8025
+ }
8026
+ /** Create a data model with just initial data (no migrations) */
8027
+ static create(initialData) {
8028
+ return new DataModel([], initialData);
8029
+ }
8030
+ /** Create from builder (internal use) */
8031
+ static _fromBuilder(steps, initialData) {
8032
+ return new DataModel(steps, initialData);
8033
+ }
8034
+ /**
8035
+ * Latest version number.
8036
+ * Version 1 = initial state, each migration adds 1.
8037
+ */
8038
+ get version() {
8039
+ return this.steps.length + 1;
8040
+ }
8041
+ /** Number of migration steps */
8042
+ get migrationCount() {
8043
+ return this.steps.length;
8044
+ }
8045
+ /** Get initial data */
8046
+ initialData() {
8047
+ return this._initialData();
8048
+ }
8049
+ /** Get default data wrapped with current version */
8050
+ getDefaultData() {
8051
+ return { version: this.version, data: this._initialData() };
8052
+ }
8053
+ /**
8054
+ * Upgrade versioned data from any version to the latest.
8055
+ * Applies only the migrations needed (skips already-applied ones).
8056
+ * If a migration fails, returns default data with a warning.
8057
+ */
8058
+ upgrade(versioned) {
8059
+ const { version: fromVersion, data } = versioned;
8060
+ if (fromVersion > this.version) {
8061
+ throw new Error(`Cannot downgrade from version ${fromVersion} to ${this.version}`);
8062
+ }
8063
+ if (fromVersion === this.version) {
8064
+ return { version: this.version, data: data };
8065
+ }
8066
+ // Apply migrations starting from (fromVersion - 1) index
8067
+ // Version 1 -> no migrations applied yet -> start at index 0
8068
+ // Version 2 -> migration[0] already applied -> start at index 1
8069
+ const startIndex = fromVersion - 1;
8070
+ const migrationsToApply = this.steps.slice(startIndex);
8071
+ let currentData = data;
8072
+ for (let i = 0; i < migrationsToApply.length; i++) {
8073
+ const stepIndex = startIndex + i;
8074
+ const fromVer = stepIndex + 1;
8075
+ const toVer = stepIndex + 2;
8076
+ try {
8077
+ currentData = migrationsToApply[i](currentData);
8078
+ }
8079
+ catch (error) {
8080
+ const errorMessage = error instanceof Error ? error.message : String(error);
8081
+ return {
8082
+ ...this.getDefaultData(),
8083
+ warning: `Migration v${fromVer}→v${toVer} failed: ${errorMessage}`,
8084
+ };
8085
+ }
8086
+ }
8087
+ return { version: this.version, data: currentData };
8088
+ }
8089
+ /**
8090
+ * Register callbacks for use in the VM.
8091
+ * Called by BlockModelV3.create() to set up internal callbacks.
8092
+ *
8093
+ * All callbacks are prefixed with `__pl_` to indicate internal SDK use:
8094
+ * - `__pl_data_initial`: returns initial data for new blocks
8095
+ * - `__pl_data_upgrade`: upgrades versioned data from any version to latest
8096
+ * - `__pl_storage_initial`: returns initial BlockStorage as JSON string
8097
+ */
8098
+ registerCallbacks() {
8099
+ tryRegisterCallback('__pl_data_initial', () => this._initialData());
8100
+ tryRegisterCallback('__pl_data_upgrade', (versioned) => this.upgrade(versioned));
8101
+ tryRegisterCallback('__pl_storage_initial', () => {
8102
+ const { version, data } = this.getDefaultData();
8103
+ const storage = createBlockStorage(data, version);
8104
+ return JSON.stringify(storage);
8105
+ });
8106
+ }
8107
+ }
8108
+
7636
8109
  var stringify = {exports: {}};
7637
8110
 
7638
8111
  var hasRequiredStringify;
@@ -7713,8 +8186,11 @@
7713
8186
  errors: z.lazy(() => ErrorShape.array()).optional(),
7714
8187
  });
7715
8188
 
7716
- const platforma = BlockModel.create('Heavy')
7717
- .withArgs({ titleArg: 'The title' })
8189
+ const dataModel = DataModel.create(() => ({ titleArgs: 'The title' }));
8190
+ const platforma = BlockModelV3.create({ dataModel, renderingMode: 'Heavy' })
8191
+ .args((data) => {
8192
+ return { titleArgs: data.titleArgs };
8193
+ })
7718
8194
  .output('allSpecs', (ctx) => ctx.resultPool.getSpecs())
7719
8195
  .sections((_ctx) => {
7720
8196
  return [{ type: 'link', href: '/', label: 'Main' }];