@milaboratories/pl-middle-layer 1.36.4 → 1.37.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 (44) hide show
  1. package/dist/index.js +19 -19
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.mjs +2031 -1918
  4. package/dist/index.mjs.map +1 -1
  5. package/dist/js_render/computable_context.d.ts +68 -0
  6. package/dist/js_render/computable_context.d.ts.map +1 -0
  7. package/dist/js_render/context.d.ts +22 -64
  8. package/dist/js_render/context.d.ts.map +1 -1
  9. package/dist/js_render/index.d.ts +2 -0
  10. package/dist/js_render/index.d.ts.map +1 -1
  11. package/dist/middle_layer/block.d.ts.map +1 -1
  12. package/dist/middle_layer/block_ctx.d.ts +5 -0
  13. package/dist/middle_layer/block_ctx.d.ts.map +1 -1
  14. package/dist/middle_layer/middle_layer.d.ts +2 -0
  15. package/dist/middle_layer/middle_layer.d.ts.map +1 -1
  16. package/dist/middle_layer/project.d.ts.map +1 -1
  17. package/dist/middle_layer/project_overview.d.ts.map +1 -1
  18. package/dist/middle_layer/util.d.ts +2 -0
  19. package/dist/middle_layer/util.d.ts.map +1 -1
  20. package/dist/model/args.d.ts +4 -2
  21. package/dist/model/args.d.ts.map +1 -1
  22. package/dist/model/project_helper.d.ts +14 -0
  23. package/dist/model/project_helper.d.ts.map +1 -0
  24. package/dist/model/project_model_util.d.ts +14 -4
  25. package/dist/model/project_model_util.d.ts.map +1 -1
  26. package/dist/mutator/project.d.ts +16 -10
  27. package/dist/mutator/project.d.ts.map +1 -1
  28. package/package.json +14 -14
  29. package/src/js_render/computable_context.ts +753 -0
  30. package/src/js_render/context.ts +32 -720
  31. package/src/js_render/index.ts +37 -3
  32. package/src/middle_layer/block.ts +2 -0
  33. package/src/middle_layer/block_ctx.ts +6 -0
  34. package/src/middle_layer/middle_layer.ts +7 -2
  35. package/src/middle_layer/project.ts +15 -17
  36. package/src/middle_layer/project_overview.ts +13 -4
  37. package/src/middle_layer/util.ts +3 -1
  38. package/src/model/args.ts +12 -6
  39. package/src/model/project_helper.ts +41 -0
  40. package/src/model/project_model_util.test.ts +13 -4
  41. package/src/model/project_model_util.ts +37 -12
  42. package/src/mutator/project.test.ts +18 -12
  43. package/src/mutator/project.ts +159 -61
  44. package/src/mutator/template/template_render.test.ts +2 -2
@@ -2,6 +2,7 @@ import type { MiddleLayerEnvironment } from '../middle_layer/middle_layer';
2
2
  import type { Code, ConfigRenderLambda } from '@platforma-sdk/model';
3
3
  import type { ComputableRenderingOps } from '@milaboratories/computable';
4
4
  import { Computable } from '@milaboratories/computable';
5
+ import type { QuickJSWASMModule } from 'quickjs-emscripten';
5
6
  import { Scope } from 'quickjs-emscripten';
6
7
  import type { DeadlineSettings } from './context';
7
8
  import { JsExecutionContext } from './context';
@@ -35,8 +36,9 @@ export function computableFromRF(
35
36
  return false;
36
37
  });
37
38
  const vm = scope.manage(runtime.newContext());
38
- const rCtx = new JsExecutionContext(scope, vm, ctx, env,
39
- (s) => { deadlineSettings = s; }, cCtx);
39
+ const rCtx = new JsExecutionContext(scope, vm,
40
+ (s) => { deadlineSettings = s; },
41
+ { computableCtx: cCtx, blockCtx: ctx, mlEnv: env });
40
42
 
41
43
  rCtx.evaluateBundle(code.content);
42
44
  const result = rCtx.runCallback(fh.handle);
@@ -49,7 +51,7 @@ export function computableFromRF(
49
51
  console.log(`Output ${fh.handle} scaffold calculated.`);
50
52
 
51
53
  return {
52
- ir: rCtx.computablesToResolve,
54
+ ir: rCtx.computableHelper!.computablesToResolve,
53
55
  postprocessValue: (resolved: Record<string, unknown>, { unstableMarker, stable }) => {
54
56
  // resolving futures
55
57
  for (const [handle, value] of Object.entries(resolved)) rCtx.runCallback(handle, value);
@@ -75,3 +77,35 @@ export function computableFromRF(
75
77
  };
76
78
  }, ops);
77
79
  }
80
+
81
+ export function executeSingleLambda(
82
+ quickJs: QuickJSWASMModule,
83
+ fh: ConfigRenderLambda,
84
+ code: Code,
85
+ ...args: unknown[]
86
+ ): unknown {
87
+ const scope = new Scope();
88
+ try {
89
+ const runtime = scope.manage(quickJs.newRuntime());
90
+ runtime.setMemoryLimit(1024 * 1024 * 8);
91
+ runtime.setMaxStackSize(1024 * 320);
92
+
93
+ let deadlineSettings: DeadlineSettings | undefined;
94
+ runtime.setInterruptHandler(() => {
95
+ if (deadlineSettings === undefined) return false;
96
+ if (Date.now() > deadlineSettings.deadline) return true;
97
+ return false;
98
+ });
99
+ const vm = scope.manage(runtime.newContext());
100
+ const rCtx = new JsExecutionContext(scope, vm,
101
+ (s) => { deadlineSettings = s; });
102
+
103
+ // Initializing the model
104
+ rCtx.evaluateBundle(code.content);
105
+
106
+ // Running the lambda
107
+ return rCtx.importObjectUniversal(rCtx.runCallback(fh.handle, ...args));
108
+ } finally {
109
+ scope.dispose();
110
+ }
111
+ }
@@ -43,7 +43,9 @@ export function blockArgsAndUiState(
43
43
  const uiState = ctx.uiState(cCtx);
44
44
  return {
45
45
  author: prj.getKeyValueAsJson<AuthorMarker>(blockArgsAuthorKey(blockId)),
46
+ // @TODO add deserialization caching
46
47
  args: deepFreeze(JSON.parse(ctx.args(cCtx))),
48
+ // @TODO add deserialization caching
47
49
  ui: uiState !== undefined ? deepFreeze(JSON.parse(uiState)) : undefined,
48
50
  };
49
51
  }
@@ -13,6 +13,12 @@ import {
13
13
  import { allBlocks } from '../model/project_model_util';
14
14
  import { ResultPool } from '../pool/result_pool';
15
15
 
16
+ export type BlockContextMaterialized = {
17
+ readonly blockId: string;
18
+ readonly args: string;
19
+ readonly uiState?: string;
20
+ };
21
+
16
22
  export type BlockContextArgsOnly = {
17
23
  readonly blockId: string;
18
24
  readonly args: (cCtx: ComputableCtx) => string;
@@ -37,6 +37,7 @@ import { V2RegistryProvider } from '../block_registry';
37
37
  import type { Dispatcher } from 'undici';
38
38
  import { RetryAgent } from 'undici';
39
39
  import { getDebugFlags } from '../debug';
40
+ import { ProjectHelper } from '../model/project_helper';
40
41
 
41
42
  export interface MiddleLayerEnvironment {
42
43
  readonly pl: PlClient;
@@ -50,6 +51,7 @@ export interface MiddleLayerEnvironment {
50
51
  readonly blockUpdateWatcher: BlockUpdateWatcher;
51
52
  readonly quickJs: QuickJSWASMModule;
52
53
  readonly driverKit: MiddleLayerDriverKit;
54
+ readonly projectHelper: ProjectHelper;
53
55
  }
54
56
 
55
57
  /**
@@ -111,7 +113,7 @@ export class MiddleLayer {
111
113
  meta: ProjectMeta,
112
114
  author?: AuthorMarker,
113
115
  ): Promise<void> {
114
- await withProjectAuthored(this.pl, rid, author, (prj) => {
116
+ await withProjectAuthored(this.env.projectHelper, this.pl, rid, author, (prj) => {
115
117
  prj.setMeta(meta);
116
118
  });
117
119
  await this.projectListTree.refreshState();
@@ -251,6 +253,8 @@ export class MiddleLayer {
251
253
  ops.frontendDownloadPath,
252
254
  );
253
255
 
256
+ const quickJs = await getQuickJS();
257
+
254
258
  const env: MiddleLayerEnvironment = {
255
259
  pl,
256
260
  signer: driverKit.signer,
@@ -266,7 +270,8 @@ export class MiddleLayer {
266
270
  http: retryHttpDispatcher,
267
271
  preferredUpdateChannel: ops.preferredUpdateChannel,
268
272
  }),
269
- quickJs: await getQuickJS(),
273
+ quickJs,
274
+ projectHelper: new ProjectHelper(quickJs),
270
275
  };
271
276
 
272
277
  const openedProjects = new WatchableValue<ResourceId[]>([]);
@@ -27,7 +27,7 @@ import { blockArgsAndUiState, blockOutputs } from './block';
27
27
  import type { FrontendData } from '../model/frontend';
28
28
  import type { ProjectStructure } from '../model/project_model';
29
29
  import { projectFieldName } from '../model/project_model';
30
- import { notEmpty } from '@milaboratories/ts-helpers';
30
+ import { cachedDeserialize, notEmpty } from '@milaboratories/ts-helpers';
31
31
  import type { BlockPackInfo } from '../model/block_pack';
32
32
  import type {
33
33
  ProjectOverview,
@@ -107,7 +107,7 @@ export class Project {
107
107
  private async refreshLoop(): Promise<void> {
108
108
  while (!this.destroyed) {
109
109
  try {
110
- await withProject(this.env.pl, this.rid, (prj) => {
110
+ await withProject(this.env.projectHelper, this.env.pl, this.rid, (prj) => {
111
111
  prj.doRefresh(this.env.ops.stagingRenderingRate);
112
112
  });
113
113
  await this.activeConfigs.getValue();
@@ -145,7 +145,7 @@ export class Project {
145
145
  const preparedBp = await this.env.bpPreparer.prepare(blockPackSpec);
146
146
  const blockCfgContainer = await this.env.bpPreparer.getBlockConfigContainer(blockPackSpec);
147
147
  const blockCfg = extractConfig(blockCfgContainer); // full content of this var should never be persisted
148
- await withProjectAuthored(this.env.pl, this.rid, author, (mut) =>
148
+ await withProjectAuthored(this.env.projectHelper, this.env.pl, this.rid, author, (mut) =>
149
149
  mut.addBlock(
150
150
  {
151
151
  id: blockId,
@@ -177,7 +177,7 @@ export class Project {
177
177
  ): Promise<void> {
178
178
  const preparedBp = await this.env.bpPreparer.prepare(blockPackSpec);
179
179
  const blockCfg = extractConfig(await this.env.bpPreparer.getBlockConfigContainer(blockPackSpec));
180
- await withProjectAuthored(this.env.pl, this.rid, author, (mut) =>
180
+ await withProjectAuthored(this.env.projectHelper, this.env.pl, this.rid, author, (mut) =>
181
181
  mut.migrateBlockPack(
182
182
  blockId,
183
183
  preparedBp,
@@ -189,7 +189,7 @@ export class Project {
189
189
 
190
190
  /** Deletes a block with all associated data. */
191
191
  public async deleteBlock(blockId: string, author?: AuthorMarker): Promise<void> {
192
- await withProjectAuthored(this.env.pl, this.rid, author, (mut) => mut.deleteBlock(blockId));
192
+ await withProjectAuthored(this.env.projectHelper, this.env.pl, this.rid, author, (mut) => mut.deleteBlock(blockId));
193
193
  this.navigationStates.deleteBlock(blockId);
194
194
  await this.projectTree.refreshState();
195
195
  }
@@ -201,7 +201,7 @@ export class Project {
201
201
  * an error will be thrown instead.
202
202
  */
203
203
  public async reorderBlocks(blocks: string[], author?: AuthorMarker): Promise<void> {
204
- await withProjectAuthored(this.env.pl, this.rid, author, (mut) => {
204
+ await withProjectAuthored(this.env.projectHelper, this.env.pl, this.rid, author, (mut) => {
205
205
  const currentStructure = mut.structure;
206
206
  if (currentStructure.groups.length !== 1)
207
207
  throw new Error('Unexpected project structure, non-sinular block group');
@@ -233,7 +233,7 @@ export class Project {
233
233
  * stale state.
234
234
  * */
235
235
  public async runBlock(blockId: string): Promise<void> {
236
- await withProject(this.env.pl, this.rid, (mut) => mut.renderProduction([blockId], true));
236
+ await withProject(this.env.projectHelper, this.env.pl, this.rid, (mut) => mut.renderProduction([blockId], true));
237
237
  await this.projectTree.refreshState();
238
238
  }
239
239
 
@@ -243,7 +243,7 @@ export class Project {
243
243
  * calculated.
244
244
  * */
245
245
  public async stopBlock(blockId: string): Promise<void> {
246
- await withProject(this.env.pl, this.rid, (mut) => mut.stopProduction(blockId));
246
+ await withProject(this.env.projectHelper, this.env.pl, this.rid, (mut) => mut.stopProduction(blockId));
247
247
  await this.projectTree.refreshState();
248
248
  }
249
249
 
@@ -262,7 +262,7 @@ export class Project {
262
262
  * in collaborative editing scenario.
263
263
  * */
264
264
  public async setBlockArgs(blockId: string, args: unknown, author?: AuthorMarker) {
265
- await withProjectAuthored(this.env.pl, this.rid, author, (mut) =>
265
+ await withProjectAuthored(this.env.projectHelper, this.env.pl, this.rid, author, (mut) =>
266
266
  mut.setArgs([{ blockId, args: canonicalize(args)! }]),
267
267
  );
268
268
  await this.projectTree.refreshState();
@@ -275,7 +275,7 @@ export class Project {
275
275
  * in collaborative editing scenario.
276
276
  * */
277
277
  public async setUiState(blockId: string, uiState: unknown, author?: AuthorMarker) {
278
- await withProjectAuthored(this.env.pl, this.rid, author, (mut) =>
278
+ await withProjectAuthored(this.env.projectHelper, this.env.pl, this.rid, author, (mut) =>
279
279
  mut.setUiState(blockId, uiState === undefined ? undefined : canonicalize(uiState)!),
280
280
  );
281
281
  await this.projectTree.refreshState();
@@ -301,7 +301,7 @@ export class Project {
301
301
  uiState: unknown,
302
302
  author?: AuthorMarker,
303
303
  ) {
304
- await withProjectAuthored(this.env.pl, this.rid, author, (mut) => {
304
+ await withProjectAuthored(this.env.projectHelper, this.env.pl, this.rid, author, (mut) => {
305
305
  mut.setArgs([{ blockId, args: canonicalize(args)! }]);
306
306
  mut.setUiState(blockId, canonicalize(uiState));
307
307
  });
@@ -310,7 +310,7 @@ export class Project {
310
310
 
311
311
  /** Update block settings */
312
312
  public async setBlockSettings(blockId: string, newValue: BlockSettings) {
313
- await withProjectAuthored(this.env.pl, this.rid, undefined, (mut) => {
313
+ await withProjectAuthored(this.env.projectHelper, this.env.pl, this.rid, undefined, (mut) => {
314
314
  mut.setBlockSettings(blockId, newValue);
315
315
  });
316
316
  await this.projectTree.refreshState();
@@ -327,10 +327,8 @@ export class Project {
327
327
  (await tx.getField(field(bpHolderRid, Pl.HolderRefField))).value,
328
328
  );
329
329
  const bpData = await tx.getResourceData(bpRid, false);
330
- const config = extractConfig((JSON.parse(
331
- Buffer.from(notEmpty(bpData.data)).toString('utf-8'),
332
- ) as BlockPackInfo).config);
333
- await withProjectAuthored(tx, this.rid, author, (prj) => {
330
+ const config = extractConfig((cachedDeserialize(notEmpty(bpData.data)) as BlockPackInfo).config);
331
+ await withProjectAuthored(this.env.projectHelper, tx, this.rid, author, (prj) => {
334
332
  prj.setArgs([{ blockId, args: canonicalize(config.initialArgs)! }]);
335
333
  prj.setUiState(blockId, canonicalize(config.initialUiState));
336
334
  });
@@ -434,7 +432,7 @@ export class Project {
434
432
 
435
433
  public static async init(env: MiddleLayerEnvironment, rid: ResourceId): Promise<Project> {
436
434
  // Doing a no-op mutation to apply all migration and schema fixes
437
- await withProject(env.pl, rid, (_) => {});
435
+ await withProject(env.projectHelper, env.pl, rid, (_) => {});
438
436
 
439
437
  // Loading project tree
440
438
  const projectTree = await SynchronizedTreeState.init(
@@ -29,11 +29,12 @@ import type { BlockSection } from '@platforma-sdk/model';
29
29
  import { computableFromCfgOrRF } from './render';
30
30
  import type { NavigationStates } from './navigation_states';
31
31
  import { getBlockPackInfo } from './util';
32
- import { resourceIdToString } from '@milaboratories/pl-client';
32
+ import { resourceIdToString, type ResourceId } from '@milaboratories/pl-client';
33
33
  import * as R from 'remeda';
34
34
 
35
35
  type BlockInfo = {
36
- currentArguments: Record<string, unknown>;
36
+ argsRid: ResourceId;
37
+ currentArguments: unknown;
37
38
  prod?: ProdState;
38
39
  };
39
40
 
@@ -127,10 +128,18 @@ export function projectOverview(
127
128
  };
128
129
  }
129
130
 
130
- infos.set(id, { currentArguments, prod });
131
+ infos.set(id, { currentArguments, prod, argsRid: cInputs.resourceInfo.id });
131
132
  }
132
133
 
133
- const currentGraph = productionGraph(structure, (id) => infos.get(id)!.currentArguments);
134
+ const currentGraph = productionGraph(structure, (id) => {
135
+ const bpInfo = getBlockPackInfo(prj, id)!;
136
+ const bInfo = infos.get(id)!;
137
+ const args = bInfo.currentArguments;
138
+ return {
139
+ args,
140
+ enrichmentTargets: env.projectHelper.getEnrichmentTargets(() => bpInfo.cfg, () => args, { argsRid: bInfo.argsRid, blockPackRid: bpInfo.bpResourceId }),
141
+ };
142
+ });
134
143
 
135
144
  const limbo = new Set(renderingState.blocksInLimbo);
136
145
 
@@ -1,5 +1,6 @@
1
1
  import type { PlTreeNodeAccessor } from '@milaboratories/pl-tree';
2
2
  import { projectFieldName } from '../model/project_model';
3
+ import type { ResourceId } from '@milaboratories/pl-client';
3
4
  import { Pl } from '@milaboratories/pl-client';
4
5
  import { ifNotUndef } from '../cfg_render/util';
5
6
  import type { BlockPackInfo } from '../model/block_pack';
@@ -7,6 +8,7 @@ import type { BlockConfig } from '@platforma-sdk/model';
7
8
  import { extractConfig } from '@platforma-sdk/model';
8
9
 
9
10
  export type BlockPackInfoAndId = {
11
+ readonly bpResourceId: ResourceId;
10
12
  /** To be added to computable keys, to force reload on config change */
11
13
  readonly bpId: string;
12
14
  /** Full block-pack info */
@@ -32,7 +34,7 @@ export function getBlockPackInfo(
32
34
  (bpAcc) => {
33
35
  const info = bpAcc.getDataAsJson<BlockPackInfo>()!;
34
36
  const cfg = extractConfig(info.config);
35
- return { bpId: bpAcc.resourceInfo.id.toString(), info, cfg };
37
+ return { bpResourceId: bpAcc.resourceInfo.id, bpId: bpAcc.resourceInfo.id.toString(), info, cfg };
36
38
  },
37
39
  );
38
40
  }
package/src/model/args.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { assertNever } from '@milaboratories/ts-helpers';
2
2
  import type { PlRef } from '@platforma-sdk/model';
3
3
 
4
- export function outputRef(blockId: string, name: string): PlRef {
5
- return { __isRef: true, blockId, name };
4
+ export function outputRef(blockId: string, name: string, requireEnrichments?: boolean): PlRef {
5
+ if (requireEnrichments) return { __isRef: true, blockId, name, requireEnrichments };
6
+ else return { __isRef: true, blockId, name };
6
7
  }
7
8
 
8
9
  export function isBlockOutputReference(obj: unknown): obj is PlRef {
@@ -32,8 +33,11 @@ function addAllReferencedBlocks(result: BlockUpstreams, node: unknown, allowed?:
32
33
  if (node === null) return;
33
34
 
34
35
  if (isBlockOutputReference(node)) {
35
- if (allowed === undefined || allowed.has(node.blockId)) result.upstreams.add(node.blockId);
36
- else result.missingReferences = true;
36
+ if (allowed === undefined || allowed.has(node.blockId)) {
37
+ result.upstreams.add(node.blockId);
38
+ if (node.requireEnrichments)
39
+ result.upstreamsRequiringEnrichments.add(node.blockId);
40
+ } else result.missingReferences = true;
37
41
  } else if (Array.isArray(node)) {
38
42
  for (const child of node) addAllReferencedBlocks(result, child, allowed);
39
43
  } else {
@@ -49,15 +53,17 @@ function addAllReferencedBlocks(result: BlockUpstreams, node: unknown, allowed?:
49
53
  }
50
54
 
51
55
  export interface BlockUpstreams {
52
- /** Direct block dependencies */
56
+ /** All direct block dependencies */
53
57
  upstreams: Set<string>;
58
+ /** Direct block dependencies which enrichments are also required by current block */
59
+ upstreamsRequiringEnrichments: Set<string>;
54
60
  /** True if not-allowed references was encountered */
55
61
  missingReferences: boolean;
56
62
  }
57
63
 
58
64
  /** Extracts all resource ids referenced by args object. */
59
65
  export function inferAllReferencedBlocks(args: unknown, allowed?: Set<string>): BlockUpstreams {
60
- const result = { upstreams: new Set<string>(), missingReferences: false };
66
+ const result = { upstreams: new Set<string>(), upstreamsRequiringEnrichments: new Set<string>(), missingReferences: false };
61
67
  addAllReferencedBlocks(result, args, allowed);
62
68
  return result;
63
69
  }
@@ -0,0 +1,41 @@
1
+ import type { BlockConfig, PlRef } from '@platforma-sdk/model';
2
+ import { LRUCache } from 'lru-cache';
3
+ import type { QuickJSWASMModule } from 'quickjs-emscripten';
4
+ import { executeSingleLambda } from '../js_render';
5
+ import type { ResourceId } from '@milaboratories/pl-client';
6
+
7
+ type EnrichmentTargetsRequest = {
8
+ blockConfig: () => BlockConfig;
9
+ args: () => unknown;
10
+ };
11
+
12
+ type EnrichmentTargetsValue = {
13
+ value: PlRef[] | undefined;
14
+ };
15
+
16
+ export class ProjectHelper {
17
+ private readonly enrichmentTargetsCache = new LRUCache<string, EnrichmentTargetsValue, EnrichmentTargetsRequest>({
18
+ max: 256,
19
+ memoMethod: (_key, _value, { context }) => {
20
+ return { value: this.calculateEnrichmentTargets(context) };
21
+ },
22
+ });
23
+
24
+ constructor(private readonly quickJs: QuickJSWASMModule) {}
25
+
26
+ private calculateEnrichmentTargets(req: EnrichmentTargetsRequest): PlRef[] | undefined {
27
+ const blockConfig = req.blockConfig();
28
+ if (blockConfig.enrichmentTargets === undefined) return undefined;
29
+ const args = req.args();
30
+ const result = executeSingleLambda(this.quickJs, blockConfig.enrichmentTargets, blockConfig.code!, args) as PlRef[];
31
+ return result;
32
+ }
33
+
34
+ public getEnrichmentTargets(blockConfig: () => BlockConfig, args: () => unknown, key?: { argsRid: ResourceId; blockPackRid: ResourceId }): PlRef[] | undefined {
35
+ const req = { blockConfig, args };
36
+ if (key === undefined)
37
+ return this.calculateEnrichmentTargets(req);
38
+ const cacheKey = `${key.argsRid}:${key.blockPackRid}`;
39
+ return this.enrichmentTargetsCache.memo(cacheKey, { context: req }).value;
40
+ }
41
+ }
@@ -4,7 +4,7 @@ import { outputRef } from './args';
4
4
  import { PlRef } from '@platforma-sdk/model';
5
5
 
6
6
  function toRefs(...ids: string[]): PlRef[] {
7
- return ids.map((id) => outputRef(id, ''));
7
+ return ids.map((id) => outputRef(id, '', true));
8
8
  }
9
9
 
10
10
  function simpleStructure(...ids: string[]): ProjectStructure {
@@ -26,7 +26,10 @@ describe('simple traverse', () => {
26
26
  inputs.set('b4', toRefs('b3'));
27
27
  inputs.set('b5', toRefs('b4'));
28
28
  inputs.set('b6', toRefs('b2', 'b4'));
29
- const pGraph1 = productionGraph(struct1, (id) => inputs.get(id) ?? null);
29
+ const pGraph1 = productionGraph(struct1, (id) => ({
30
+ args: inputs.get(id) ?? null,
31
+ enrichmentTargets: undefined,
32
+ }));
30
33
 
31
34
  test.each([
32
35
  { roots: ['b2'], expectedDirectUpstreams: ['b1'], expectedDirectDownstreams: ['b6'], expectedUpstreams: ['b1'], expectedDownstreams: ['b6'] },
@@ -51,7 +54,10 @@ describe('simple diff', () => {
51
54
  inputs.set('b2', toRefs('b1'));
52
55
  inputs.set('b4', toRefs('b3'));
53
56
  const sGraph1 = stagingGraph(struct1);
54
- const pGraph1 = productionGraph(struct1, (id) => inputs.get(id) ?? null);
57
+ const pGraph1 = productionGraph(struct1, (id) => ({
58
+ args: inputs.get(id) ?? null,
59
+ enrichmentTargets: undefined,
60
+ }));
55
61
 
56
62
  test.each([
57
63
  { struct2a: ['b1', 'b2', 'b3', 'b4'], expectedS: [], expectedP: [] },
@@ -79,7 +85,10 @@ describe('simple diff', () => {
79
85
  ])('$struct2a', ({ struct2a, expectedS, expectedP, onlyA, onlyB }) => {
80
86
  const struct2: ProjectStructure = simpleStructure(...struct2a);
81
87
  const sGraph2 = stagingGraph(struct2);
82
- const pGraph2 = productionGraph(struct2, (id) => inputs.get(id) ?? null);
88
+ const pGraph2 = productionGraph(struct2, (id) => ({
89
+ args: inputs.get(id) ?? null,
90
+ enrichmentTargets: undefined,
91
+ }));
83
92
  const sDiff = graphDiff(sGraph1, sGraph2);
84
93
  const pDiff = graphDiff(pGraph1, pGraph2);
85
94
  expect(sDiff.onlyInA).toEqual(new Set(onlyA ?? []));
@@ -1,6 +1,7 @@
1
1
  import type { Block, ProjectStructure } from './project_model';
2
2
  import type { Optional, Writable } from 'utility-types';
3
3
  import { inferAllReferencedBlocks } from './args';
4
+ import type { PlRef } from '@milaboratories/pl-model-common';
4
5
 
5
6
  export function allBlocks(structure: ProjectStructure): Iterable<Block> {
6
7
  return {
@@ -19,10 +20,16 @@ export interface BlockGraphNode {
19
20
  readonly directUpstream: Set<string>;
20
21
  /** Downstreams, calculated accounting for potential indirect block dependencies */
21
22
  readonly downstream: Set<string>;
23
+ /** Downstream blocks enriching current block's outputs */
24
+ readonly enrichments: Set<string>;
22
25
  /** Direct downstreams listing our block in it's args */
23
26
  readonly directDownstream: Set<string>;
27
+ /** Upstream blocks that current block may enrich with its exports */
28
+ readonly enrichmentTargets: Set<string>;
24
29
  }
25
30
 
31
+ export type BlockGraphDirection = 'upstream' | 'downstream' | 'directUpstream' | 'directDownstream' | 'enrichments' | 'enrichmentTargets';
32
+
26
33
  export class BlockGraph {
27
34
  /** Nodes are stored in the map in topological order */
28
35
  public readonly nodes: Map<string, BlockGraphNode>;
@@ -32,7 +39,7 @@ export class BlockGraph {
32
39
  }
33
40
 
34
41
  public traverseIds(
35
- direction: 'upstream' | 'downstream' | 'directUpstream' | 'directDownstream',
42
+ direction: BlockGraphDirection,
36
43
  ...rootBlockIds: string[]
37
44
  ): Set<string> {
38
45
  const all = new Set<string>();
@@ -41,7 +48,7 @@ export class BlockGraph {
41
48
  }
42
49
 
43
50
  public traverseIdsExcludingRoots(
44
- direction: 'upstream' | 'downstream' | 'directUpstream' | 'directDownstream',
51
+ direction: BlockGraphDirection,
45
52
  ...rootBlockIds: string[]
46
53
  ): Set<string> {
47
54
  const result = this.traverseIds(direction, ...rootBlockIds);
@@ -50,7 +57,7 @@ export class BlockGraph {
50
57
  }
51
58
 
52
59
  public traverse(
53
- direction: 'upstream' | 'downstream' | 'directUpstream' | 'directDownstream',
60
+ direction: BlockGraphDirection,
54
61
  rootBlockIds: string[],
55
62
  cb: (node: BlockGraphNode) => void,
56
63
  ): void {
@@ -75,7 +82,7 @@ export class BlockGraph {
75
82
  }
76
83
 
77
84
  export function stagingGraph(structure: ProjectStructure) {
78
- type WNode = Optional<Writable<BlockGraphNode>, 'upstream' | 'downstream' | 'directUpstream' | 'directDownstream'>;
85
+ type WNode = Optional<Writable<BlockGraphNode>, BlockGraphDirection>;
79
86
  const result = new Map<string, WNode>();
80
87
 
81
88
  // Simple dependency graph from previous to next
@@ -92,9 +99,11 @@ export function stagingGraph(structure: ProjectStructure) {
92
99
  result.set(id, current);
93
100
  if (previous === undefined) {
94
101
  current.directUpstream = current.upstream = new Set<string>();
102
+ current.enrichments = current.enrichmentTargets = new Set<string>();
95
103
  } else {
96
104
  current.directUpstream = current.upstream = new Set<string>([previous.id]);
97
105
  previous.directDownstream = previous.downstream = new Set<string>([current.id]);
106
+ previous.enrichments = previous.enrichmentTargets = new Set<string>();
98
107
  }
99
108
 
100
109
  previous = current;
@@ -104,9 +113,14 @@ export function stagingGraph(structure: ProjectStructure) {
104
113
  return new BlockGraph(result as Map<string, BlockGraphNode>);
105
114
  }
106
115
 
116
+ export type ProductionGraphBlockInfo = {
117
+ args: unknown;
118
+ enrichmentTargets: PlRef[] | undefined;
119
+ };
120
+
107
121
  export function productionGraph(
108
122
  structure: ProjectStructure,
109
- argsProvider: (blockId: string) => unknown,
123
+ infoProvider: (blockId: string) => ProductionGraphBlockInfo | undefined,
110
124
  ): BlockGraph {
111
125
  const resultMap = new Map<string, BlockGraphNode>();
112
126
  // result graph is constructed to be able to perform traversal on incomplete graph
@@ -118,16 +132,19 @@ export function productionGraph(
118
132
  // those dependencies that are possible under current topology
119
133
  const allAbove = new Set<string>();
120
134
  for (const { id } of allBlocks(structure)) {
121
- const args = argsProvider(id);
135
+ const info = infoProvider(id);
122
136
 
123
137
  // skipping those blocks for which we don't have args
124
- if (args === undefined) continue;
138
+ if (info === undefined) continue;
125
139
 
126
- const directUpstreams = inferAllReferencedBlocks(args, allAbove);
140
+ const references = inferAllReferencedBlocks(info.args, allAbove);
127
141
 
128
142
  // The algorithm here adds all downstream blocks of direct upstreams as potential upstreams.
129
143
  // They may produce additional columns, anchored in our direct upstream, those columns might be needed by the workflow.
130
- const potentialUpstreams = resultGraph.traverseIds('directDownstream', ...directUpstreams.upstreams);
144
+ const potentialUpstreams = new Set([
145
+ ...references.upstreams,
146
+ ...resultGraph.traverseIds('enrichments', ...references.upstreamsRequiringEnrichments),
147
+ ]);
131
148
 
132
149
  // To minimize complexity of the graph, we leave only the closest upstreams, removing all their transitive dependencies,
133
150
  // relying on the traversal mechanisms in BContexts and on UI level to connect our block with all upstreams
@@ -141,17 +158,25 @@ export function productionGraph(
141
158
  }
142
159
  }
143
160
 
161
+ // default assumption is that all direct upstreams are enrichment targets
162
+ const enrichmentTargets = info.enrichmentTargets === undefined
163
+ ? new Set(references.upstreams)
164
+ : new Set(info.enrichmentTargets.map((t) => t.blockId));
165
+
144
166
  const node: BlockGraphNode = {
145
167
  id,
146
- missingReferences: directUpstreams.missingReferences,
168
+ missingReferences: references.missingReferences,
147
169
  upstream: upstreams,
148
- directUpstream: directUpstreams.upstreams,
170
+ directUpstream: references.upstreams,
171
+ enrichmentTargets,
149
172
  downstream: new Set<string>(), // will be populated from downstream blocks
150
173
  directDownstream: new Set<string>(), // will be populated from downstream blocks
174
+ enrichments: new Set<string>(), // will be populated from downstream blocks
151
175
  };
152
176
  resultMap.set(id, node);
153
- directUpstreams.upstreams.forEach((dep) => resultMap.get(dep)!.directDownstream.add(id));
177
+ references.upstreams.forEach((dep) => resultMap.get(dep)!.directDownstream.add(id));
154
178
  upstreams.forEach((dep) => resultMap.get(dep)!.downstream.add(id));
179
+ enrichmentTargets.forEach((dep) => resultMap.get(dep)?.enrichments.add(id));
155
180
  allAbove.add(id);
156
181
  }
157
182