@milaboratories/pl-middle-layer 1.10.12

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 (179) hide show
  1. package/dist/block_registry/index.d.ts +4 -0
  2. package/dist/block_registry/index.d.ts.map +1 -0
  3. package/dist/block_registry/registry.d.ts +37 -0
  4. package/dist/block_registry/registry.d.ts.map +1 -0
  5. package/dist/block_registry/registry_spec.d.ts +12 -0
  6. package/dist/block_registry/registry_spec.d.ts.map +1 -0
  7. package/dist/block_registry/watcher.d.ts +15 -0
  8. package/dist/block_registry/watcher.d.ts.map +1 -0
  9. package/dist/block_registry/well_known_registries.d.ts +4 -0
  10. package/dist/block_registry/well_known_registries.d.ts.map +1 -0
  11. package/dist/cfg_render/executor.d.ts +8 -0
  12. package/dist/cfg_render/executor.d.ts.map +1 -0
  13. package/dist/cfg_render/operation.d.ts +29 -0
  14. package/dist/cfg_render/operation.d.ts.map +1 -0
  15. package/dist/cfg_render/renderer.d.ts +6 -0
  16. package/dist/cfg_render/renderer.d.ts.map +1 -0
  17. package/dist/cfg_render/traverse.d.ts +3 -0
  18. package/dist/cfg_render/traverse.d.ts.map +1 -0
  19. package/dist/cfg_render/util.d.ts +5 -0
  20. package/dist/cfg_render/util.d.ts.map +1 -0
  21. package/dist/dev/index.d.ts +21 -0
  22. package/dist/dev/index.d.ts.map +1 -0
  23. package/dist/dev/util.d.ts +3 -0
  24. package/dist/dev/util.d.ts.map +1 -0
  25. package/dist/index.d.ts +13 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +2 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/index.mjs +3587 -0
  30. package/dist/index.mjs.map +1 -0
  31. package/dist/js_render/context.d.ts +68 -0
  32. package/dist/js_render/context.d.ts.map +1 -0
  33. package/dist/js_render/index.d.ts +6 -0
  34. package/dist/js_render/index.d.ts.map +1 -0
  35. package/dist/middle_layer/active_cfg.d.ts +6 -0
  36. package/dist/middle_layer/active_cfg.d.ts.map +1 -0
  37. package/dist/middle_layer/block.d.ts +9 -0
  38. package/dist/middle_layer/block.d.ts.map +1 -0
  39. package/dist/middle_layer/block_ctx.d.ts +20 -0
  40. package/dist/middle_layer/block_ctx.d.ts.map +1 -0
  41. package/dist/middle_layer/block_ctx_unsafe.d.ts +16 -0
  42. package/dist/middle_layer/block_ctx_unsafe.d.ts.map +1 -0
  43. package/dist/middle_layer/driver_kit.d.ts +31 -0
  44. package/dist/middle_layer/driver_kit.d.ts.map +1 -0
  45. package/dist/middle_layer/frontend_path.d.ts +6 -0
  46. package/dist/middle_layer/frontend_path.d.ts.map +1 -0
  47. package/dist/middle_layer/index.d.ts +5 -0
  48. package/dist/middle_layer/index.d.ts.map +1 -0
  49. package/dist/middle_layer/middle_layer.d.ts +78 -0
  50. package/dist/middle_layer/middle_layer.d.ts.map +1 -0
  51. package/dist/middle_layer/navigation_states.d.ts +10 -0
  52. package/dist/middle_layer/navigation_states.d.ts.map +1 -0
  53. package/dist/middle_layer/ops.d.ts +64 -0
  54. package/dist/middle_layer/ops.d.ts.map +1 -0
  55. package/dist/middle_layer/project.d.ts +110 -0
  56. package/dist/middle_layer/project.d.ts.map +1 -0
  57. package/dist/middle_layer/project_list.d.ts +11 -0
  58. package/dist/middle_layer/project_list.d.ts.map +1 -0
  59. package/dist/middle_layer/project_overview.d.ts +8 -0
  60. package/dist/middle_layer/project_overview.d.ts.map +1 -0
  61. package/dist/middle_layer/render.d.ts +6 -0
  62. package/dist/middle_layer/render.d.ts.map +1 -0
  63. package/dist/middle_layer/types.d.ts +11 -0
  64. package/dist/middle_layer/types.d.ts.map +1 -0
  65. package/dist/middle_layer/util.d.ts +3 -0
  66. package/dist/middle_layer/util.d.ts.map +1 -0
  67. package/dist/model/args.d.ts +12 -0
  68. package/dist/model/args.d.ts.map +1 -0
  69. package/dist/model/block_pack.d.ts +8 -0
  70. package/dist/model/block_pack.d.ts.map +1 -0
  71. package/dist/model/block_pack_spec.d.ts +40 -0
  72. package/dist/model/block_pack_spec.d.ts.map +1 -0
  73. package/dist/model/frontend.d.ts +10 -0
  74. package/dist/model/frontend.d.ts.map +1 -0
  75. package/dist/model/index.d.ts +3 -0
  76. package/dist/model/index.d.ts.map +1 -0
  77. package/dist/model/project_model.d.ts +67 -0
  78. package/dist/model/project_model.d.ts.map +1 -0
  79. package/dist/model/project_model_util.d.ts +29 -0
  80. package/dist/model/project_model_util.d.ts.map +1 -0
  81. package/dist/model/template_spec.d.ts +16 -0
  82. package/dist/model/template_spec.d.ts.map +1 -0
  83. package/dist/mutator/block-pack/block_pack.d.ts +17 -0
  84. package/dist/mutator/block-pack/block_pack.d.ts.map +1 -0
  85. package/dist/mutator/block-pack/frontend.d.ts +4 -0
  86. package/dist/mutator/block-pack/frontend.d.ts.map +1 -0
  87. package/dist/mutator/context_export.d.ts +9 -0
  88. package/dist/mutator/context_export.d.ts.map +1 -0
  89. package/dist/mutator/project.d.ts +121 -0
  90. package/dist/mutator/project.d.ts.map +1 -0
  91. package/dist/mutator/template/render_block.d.ts +32 -0
  92. package/dist/mutator/template/render_block.d.ts.map +1 -0
  93. package/dist/mutator/template/render_template.d.ts +12 -0
  94. package/dist/mutator/template/render_template.d.ts.map +1 -0
  95. package/dist/mutator/template/template_loading.d.ts +13 -0
  96. package/dist/mutator/template/template_loading.d.ts.map +1 -0
  97. package/dist/pool/data.d.ts +24 -0
  98. package/dist/pool/data.d.ts.map +1 -0
  99. package/dist/pool/driver.d.ts +22 -0
  100. package/dist/pool/driver.d.ts.map +1 -0
  101. package/dist/pool/index.d.ts +3 -0
  102. package/dist/pool/index.d.ts.map +1 -0
  103. package/dist/pool/p_object_collection.d.ts +29 -0
  104. package/dist/pool/p_object_collection.d.ts.map +1 -0
  105. package/dist/pool/ref_count_pool.d.ts +25 -0
  106. package/dist/pool/ref_count_pool.d.ts.map +1 -0
  107. package/dist/pool/result_pool.d.ts +25 -0
  108. package/dist/pool/result_pool.d.ts.map +1 -0
  109. package/dist/test/block_packs.d.ts +6 -0
  110. package/dist/test/block_packs.d.ts.map +1 -0
  111. package/dist/test/explicit_templates.d.ts +3 -0
  112. package/dist/test/explicit_templates.d.ts.map +1 -0
  113. package/dist/test/known_templates.d.ts +6 -0
  114. package/dist/test/known_templates.d.ts.map +1 -0
  115. package/package.json +55 -0
  116. package/src/block_registry/index.ts +3 -0
  117. package/src/block_registry/registry.test.ts +35 -0
  118. package/src/block_registry/registry.ts +180 -0
  119. package/src/block_registry/registry_spec.ts +13 -0
  120. package/src/block_registry/watcher.ts +72 -0
  121. package/src/block_registry/well_known_registries.ts +13 -0
  122. package/src/cfg_render/executor.test.ts +120 -0
  123. package/src/cfg_render/executor.ts +253 -0
  124. package/src/cfg_render/operation.ts +38 -0
  125. package/src/cfg_render/renderer.ts +540 -0
  126. package/src/cfg_render/traverse.ts +58 -0
  127. package/src/cfg_render/util.ts +29 -0
  128. package/src/dev/index.ts +89 -0
  129. package/src/dev/util.ts +13 -0
  130. package/src/index.ts +21 -0
  131. package/src/js_render/context.ts +768 -0
  132. package/src/js_render/index.ts +41 -0
  133. package/src/middle_layer/active_cfg.ts +56 -0
  134. package/src/middle_layer/block.ts +70 -0
  135. package/src/middle_layer/block_ctx.ts +90 -0
  136. package/src/middle_layer/block_ctx_unsafe.ts +29 -0
  137. package/src/middle_layer/driver_kit.ts +107 -0
  138. package/src/middle_layer/frontend_path.ts +83 -0
  139. package/src/middle_layer/index.ts +4 -0
  140. package/src/middle_layer/middle_layer.test.ts +720 -0
  141. package/src/middle_layer/middle_layer.ts +235 -0
  142. package/src/middle_layer/navigation_states.ts +48 -0
  143. package/src/middle_layer/ops.ts +147 -0
  144. package/src/middle_layer/project.ts +380 -0
  145. package/src/middle_layer/project_list.ts +59 -0
  146. package/src/middle_layer/project_overview.ts +220 -0
  147. package/src/middle_layer/render.test.ts +129 -0
  148. package/src/middle_layer/render.ts +19 -0
  149. package/src/middle_layer/types.ts +16 -0
  150. package/src/middle_layer/util.ts +22 -0
  151. package/src/model/args.ts +62 -0
  152. package/src/model/block_pack.ts +8 -0
  153. package/src/model/block_pack_spec.ts +52 -0
  154. package/src/model/frontend.ts +10 -0
  155. package/src/model/index.ts +2 -0
  156. package/src/model/project_model.test.ts +26 -0
  157. package/src/model/project_model.ts +142 -0
  158. package/src/model/project_model_util.test.ts +88 -0
  159. package/src/model/project_model_util.ts +169 -0
  160. package/src/model/template_spec.ts +18 -0
  161. package/src/mutator/block-pack/block_pack.test.ts +53 -0
  162. package/src/mutator/block-pack/block_pack.ts +187 -0
  163. package/src/mutator/block-pack/frontend.ts +29 -0
  164. package/src/mutator/context_export.ts +25 -0
  165. package/src/mutator/project.test.ts +272 -0
  166. package/src/mutator/project.ts +1112 -0
  167. package/src/mutator/template/render_block.ts +91 -0
  168. package/src/mutator/template/render_template.ts +40 -0
  169. package/src/mutator/template/template_loading.ts +77 -0
  170. package/src/mutator/template/template_render.test.ts +272 -0
  171. package/src/pool/data.ts +239 -0
  172. package/src/pool/driver.ts +325 -0
  173. package/src/pool/index.ts +2 -0
  174. package/src/pool/p_object_collection.ts +122 -0
  175. package/src/pool/ref_count_pool.ts +76 -0
  176. package/src/pool/result_pool.ts +284 -0
  177. package/src/test/block_packs.ts +23 -0
  178. package/src/test/explicit_templates.ts +8 -0
  179. package/src/test/known_templates.ts +24 -0
@@ -0,0 +1,1112 @@
1
+ import {
2
+ AnyRef,
3
+ AnyResourceRef,
4
+ BasicResourceData,
5
+ ensureResourceIdNotNull,
6
+ field,
7
+ isNotNullResourceId,
8
+ isNullResourceId,
9
+ isResource,
10
+ isResourceRef,
11
+ Pl,
12
+ PlClient,
13
+ PlTransaction,
14
+ ResourceId,
15
+ ResourceRef
16
+ } from '@milaboratories/pl-client';
17
+ import { createRenderHeavyBlock, createBContextFromUpstreams } from './template/render_block';
18
+ import {
19
+ Block,
20
+ BlockRenderingStateKey,
21
+ ProjectStructure,
22
+ ProjectStructureKey,
23
+ parseProjectField,
24
+ ProjectField,
25
+ projectFieldName,
26
+ ProjectRenderingState,
27
+ SchemaVersionCurrent,
28
+ SchemaVersionKey,
29
+ ProjectResourceType,
30
+ InitialBlockStructure,
31
+ InitialProjectRenderingState,
32
+ ProjectMetaKey,
33
+ InitialBlockMeta,
34
+ parseBlockFrontendStateKey,
35
+ blockFrontendStateKey,
36
+ blockArgsAuthorKey,
37
+ ProjectLastModifiedTimestamp,
38
+ ProjectCreatedTimestamp,
39
+ ProjectStructureAuthorKey,
40
+ getServiceTemplateField
41
+ } from '../model/project_model';
42
+ import { BlockPackTemplateField, createBlockPack } from './block-pack/block_pack';
43
+ import {
44
+ allBlocks,
45
+ BlockGraph,
46
+ graphDiff,
47
+ productionGraph,
48
+ stagingGraph
49
+ } from '../model/project_model_util';
50
+ import { BlockPackSpecPrepared } from '../model';
51
+ import { notEmpty } from '@milaboratories/ts-helpers';
52
+ import { AuthorMarker, ProjectMeta } from '@milaboratories/pl-model-middle-layer';
53
+ import Denque from 'denque';
54
+ import { exportContext, getPreparedExportTemplateEnvelope } from './context_export';
55
+ import { loadTemplate } from './template/template_loading';
56
+
57
+ type FieldStatus = 'NotReady' | 'Ready' | 'Error';
58
+
59
+ interface BlockFieldState {
60
+ modCount: number;
61
+ ref?: AnyRef;
62
+ status?: FieldStatus;
63
+ value?: Uint8Array;
64
+ }
65
+
66
+ type BlockFieldStates = Partial<Record<ProjectField['fieldName'], BlockFieldState>>;
67
+
68
+ interface BlockInfoState {
69
+ readonly id: string;
70
+ readonly fields: BlockFieldStates;
71
+ }
72
+
73
+ function cached<ModId, T>(modIdCb: () => ModId, valueCb: () => T): () => T {
74
+ let initialized = false;
75
+ let lastModId: ModId | undefined = undefined;
76
+ let value: T | undefined = undefined;
77
+ return () => {
78
+ if (!initialized) {
79
+ initialized = true;
80
+ lastModId = modIdCb();
81
+ value = valueCb();
82
+ return value as T;
83
+ }
84
+ const currentModId = modIdCb();
85
+ if (lastModId !== currentModId) {
86
+ lastModId = currentModId;
87
+ value = valueCb();
88
+ }
89
+ return valueCb() as T;
90
+ };
91
+ }
92
+
93
+ class BlockInfo {
94
+ constructor(
95
+ public readonly id: string,
96
+ public readonly fields: BlockFieldStates
97
+ ) {}
98
+
99
+ public check() {
100
+ // state assertions
101
+
102
+ if ((this.fields.prodOutput === undefined) !== (this.fields.prodCtx === undefined))
103
+ throw new Error('inconsistent prod fields');
104
+
105
+ if ((this.fields.stagingOutput === undefined) !== (this.fields.stagingCtx === undefined))
106
+ throw new Error('inconsistent stage fields');
107
+
108
+ if (
109
+ (this.fields.prodOutputPrevious === undefined) !==
110
+ (this.fields.prodCtxPrevious === undefined)
111
+ )
112
+ throw new Error('inconsistent prod cache fields');
113
+
114
+ if (
115
+ (this.fields.stagingOutputPrevious === undefined) !==
116
+ (this.fields.stagingCtxPrevious === undefined)
117
+ )
118
+ throw new Error('inconsistent stage cache fields');
119
+
120
+ if (this.fields.blockPack === undefined) throw new Error('no block pack field');
121
+
122
+ if (this.fields.currentArgs === undefined) throw new Error('no current args field');
123
+ }
124
+
125
+ private readonly currentInputsC = cached(
126
+ () => this.fields.currentArgs!.modCount,
127
+ () => JSON.parse(Buffer.from(this.fields.currentArgs!.value!).toString())
128
+ );
129
+ private readonly actualProductionInputsC = cached(
130
+ () => this.fields.prodArgs?.modCount,
131
+ () => {
132
+ const bin = this.fields.prodArgs?.value;
133
+ if (bin === undefined) return undefined;
134
+ return JSON.parse(Buffer.from(bin).toString());
135
+ }
136
+ );
137
+
138
+ get currentInputs(): any {
139
+ return this.currentInputsC();
140
+ }
141
+
142
+ get stagingRendered(): boolean {
143
+ return this.fields.stagingCtx !== undefined;
144
+ }
145
+
146
+ get productionRendered(): boolean {
147
+ return this.fields.prodCtx !== undefined;
148
+ }
149
+
150
+ private readonly productionStaleC: () => boolean = cached(
151
+ () => `${this.fields.currentArgs!.modCount}_${this.fields.prodArgs?.modCount}`,
152
+ () =>
153
+ this.fields.prodArgs === undefined ||
154
+ Buffer.compare(this.fields.currentArgs!.value!, this.fields.prodArgs.value!) !== 0
155
+ );
156
+
157
+ get productionStale(): boolean {
158
+ return this.productionRendered && this.productionStaleC();
159
+ }
160
+
161
+ get requireProductionRendering(): boolean {
162
+ return !this.productionRendered || this.productionStaleC();
163
+ }
164
+
165
+ get actualProductionInputs(): any | undefined {
166
+ return this.actualProductionInputsC();
167
+ }
168
+
169
+ public getTemplate(tx: PlTransaction): AnyRef {
170
+ return tx.getFutureFieldValue(
171
+ Pl.unwrapHolder(tx, this.fields.blockPack!.ref!),
172
+ BlockPackTemplateField,
173
+ 'Input'
174
+ );
175
+ }
176
+ }
177
+
178
+ export interface NewBlockSpec {
179
+ blockPack: BlockPackSpecPrepared;
180
+ args: string;
181
+ }
182
+
183
+ const NoNewBlocks = (blockId: string) => {
184
+ throw new Error(`No new block info for ${blockId}`);
185
+ };
186
+
187
+ export interface SetArgsRequest {
188
+ blockId: string;
189
+ args: string;
190
+ }
191
+
192
+ type GraphInfoFields =
193
+ | 'stagingUpstream'
194
+ | 'stagingDownstream'
195
+ | 'futureProductionUpstream'
196
+ | 'futureProductionDownstream'
197
+ | 'actualProductionUpstream'
198
+ | 'actualProductionDownstream';
199
+
200
+ export class ProjectMutator {
201
+ private globalModCount = 0;
202
+ private fieldsChanged: boolean = false;
203
+
204
+ //
205
+ // Change trackers
206
+ //
207
+
208
+ private lastModifiedChanged = false;
209
+ private structureChanged = false;
210
+ private metaChanged = false;
211
+ private renderingStateChanged = false;
212
+ private readonly changedBlockFrontendStates = new Set<string>();
213
+
214
+ /** Set blocks will be assigned current mutator author marker on save */
215
+ private readonly blocksWithChangedInputs = new Set<string>();
216
+
217
+ constructor(
218
+ public readonly rid: ResourceId,
219
+ private readonly tx: PlTransaction,
220
+ private readonly author: AuthorMarker | undefined,
221
+ private readonly schema: string,
222
+ private lastModified: number,
223
+ private meta: ProjectMeta,
224
+ private struct: ProjectStructure,
225
+ private readonly renderingState: Omit<ProjectRenderingState, 'blocksInLimbo'>,
226
+ private readonly blocksInLimbo: Set<string>,
227
+ private readonly blockInfos: Map<string, BlockInfo>,
228
+ private readonly blockFrontendStates: Map<string, string>,
229
+ private readonly ctxExportTplHolder: AnyResourceRef
230
+ ) {}
231
+
232
+ private fixProblems() {
233
+ this.blockInfos.forEach((blockInfo) => {
234
+ if (
235
+ blockInfo.fields.prodArgs === undefined ||
236
+ blockInfo.fields.prodOutput === undefined ||
237
+ blockInfo.fields.prodCtx === undefined
238
+ )
239
+ this.deleteBlockFields(blockInfo.id, 'prodArgs', 'prodOutput', 'prodCtx');
240
+ });
241
+ }
242
+
243
+ get wasModified(): boolean {
244
+ return (
245
+ this.lastModifiedChanged ||
246
+ this.structureChanged ||
247
+ this.fieldsChanged ||
248
+ this.metaChanged ||
249
+ this.renderingStateChanged ||
250
+ this.changedBlockFrontendStates.size > 0
251
+ );
252
+ }
253
+
254
+ get structure(): ProjectStructure {
255
+ // clone
256
+ return JSON.parse(JSON.stringify(this.struct));
257
+ }
258
+
259
+ //
260
+ // Graph calculation
261
+ //
262
+
263
+ private stagingGraph: BlockGraph | undefined = undefined;
264
+ private pendingProductionGraph: BlockGraph | undefined = undefined;
265
+ private actualProductionGraph: BlockGraph | undefined = undefined;
266
+
267
+ private getStagingGraph(): BlockGraph {
268
+ if (this.stagingGraph === undefined) this.stagingGraph = stagingGraph(this.struct);
269
+ return this.stagingGraph;
270
+ }
271
+
272
+ private getPendingProductionGraph(): BlockGraph {
273
+ if (this.pendingProductionGraph === undefined)
274
+ this.pendingProductionGraph = productionGraph(
275
+ this.struct,
276
+ (blockId) => this.getBlockInfo(blockId).currentInputs
277
+ );
278
+ return this.pendingProductionGraph;
279
+ }
280
+
281
+ private getActualProductionGraph(): BlockGraph {
282
+ if (this.actualProductionGraph === undefined)
283
+ this.actualProductionGraph = productionGraph(
284
+ this.struct,
285
+ (blockId) => this.getBlockInfo(blockId).actualProductionInputs
286
+ );
287
+ return this.actualProductionGraph;
288
+ }
289
+
290
+ //
291
+ // Generic helpers to interact with project state
292
+ //
293
+
294
+ private getBlockInfo(blockId: string): BlockInfo {
295
+ return notEmpty(this.blockInfos.get(blockId));
296
+ }
297
+
298
+ private getBlock(blockId: string): Block {
299
+ for (const block of allBlocks(this.struct)) if (block.id === blockId) return block;
300
+ throw new Error('block not found');
301
+ }
302
+
303
+ private setBlockFieldObj(
304
+ blockId: string,
305
+ fieldName: keyof BlockFieldStates,
306
+ state: Omit<BlockFieldState, 'modCount'>
307
+ ) {
308
+ const fid = field(this.rid, projectFieldName(blockId, fieldName));
309
+
310
+ if (state.ref === undefined) throw new Error("Can't set value with empty ref");
311
+
312
+ if (this.getBlockInfo(blockId).fields[fieldName] === undefined)
313
+ this.tx.createField(fid, 'Dynamic', state.ref);
314
+ else this.tx.setField(fid, state.ref);
315
+
316
+ this.getBlockInfo(blockId).fields[fieldName] = {
317
+ modCount: this.globalModCount++,
318
+ ...state
319
+ };
320
+
321
+ this.fieldsChanged = true;
322
+ }
323
+
324
+ private setBlockField(
325
+ blockId: string,
326
+ fieldName: keyof BlockFieldStates,
327
+ ref: AnyRef,
328
+ status: FieldStatus,
329
+ value?: Uint8Array
330
+ ) {
331
+ this.setBlockFieldObj(blockId, fieldName, { ref, status, value });
332
+ }
333
+
334
+ private deleteBlockFields(blockId: string, ...fieldNames: (keyof BlockFieldStates)[]): boolean {
335
+ let deleted = false;
336
+ const info = this.getBlockInfo(blockId);
337
+ for (const fieldName of fieldNames) {
338
+ const fields = info.fields;
339
+ if (!(fieldName in fields)) continue;
340
+ this.tx.removeField(field(this.rid, projectFieldName(blockId, fieldName)));
341
+ delete fields[fieldName];
342
+ this.fieldsChanged = true;
343
+ deleted = true;
344
+ }
345
+ return deleted;
346
+ }
347
+
348
+ private updateLastModified() {
349
+ this.lastModified = Date.now();
350
+ this.lastModifiedChanged = true;
351
+ }
352
+
353
+ //
354
+ // Main project actions
355
+ //
356
+
357
+ private resetStagingRefreshTimestamp() {
358
+ this.renderingState.stagingRefreshTimestamp = Date.now();
359
+ this.renderingStateChanged = true;
360
+ }
361
+
362
+ private resetStaging(blockId: string): void {
363
+ const fields = this.getBlockInfo(blockId).fields;
364
+ if (
365
+ fields.stagingOutput?.status === 'Ready' &&
366
+ fields.stagingCtx?.status === 'Ready' &&
367
+ fields.stagingUiCtx?.status === 'Ready'
368
+ ) {
369
+ this.setBlockFieldObj(blockId, 'stagingOutputPrevious', fields.stagingOutput);
370
+ this.setBlockFieldObj(blockId, 'stagingCtxPrevious', fields.stagingCtx);
371
+ this.setBlockFieldObj(blockId, 'stagingUiCtxPrevious', fields.stagingUiCtx);
372
+ }
373
+ if (this.deleteBlockFields(blockId, 'stagingOutput', 'stagingCtx', 'stagingUiCtx'))
374
+ this.resetStagingRefreshTimestamp();
375
+ }
376
+
377
+ private resetProduction(blockId: string): void {
378
+ const fields = this.getBlockInfo(blockId).fields;
379
+ if (
380
+ fields.prodOutput?.status === 'Ready' &&
381
+ fields.prodCtx?.status === 'Ready' &&
382
+ fields.prodUiCtx?.status === 'Ready'
383
+ ) {
384
+ this.setBlockFieldObj(blockId, 'prodOutputPrevious', fields.prodOutput);
385
+ this.setBlockFieldObj(blockId, 'prodCtxPrevious', fields.prodCtx);
386
+ this.setBlockFieldObj(blockId, 'prodUiCtxPrevious', fields.prodUiCtx);
387
+ }
388
+ this.deleteBlockFields(blockId, 'prodOutput', 'prodCtx', 'prodUiCtx', 'prodArgs');
389
+ }
390
+
391
+ /** Running blocks are reset, already computed moved to limbo. Returns if
392
+ * either of the actions were actually performed. */
393
+ private resetOrLimboProduction(blockId: string): boolean {
394
+ const fields = this.getBlockInfo(blockId).fields;
395
+ if (fields.prodOutput?.status === 'Ready' && fields.prodCtx?.status === 'Ready') {
396
+ if (this.blocksInLimbo.has(blockId))
397
+ // we are already in limbo
398
+ return false;
399
+
400
+ // limbo
401
+ this.blocksInLimbo.add(blockId);
402
+ this.renderingStateChanged = true;
403
+
404
+ // doing some gc
405
+ this.deleteBlockFields(blockId, 'prodOutputPrevious', 'prodCtxPrevious', 'prodUiCtxPrevious');
406
+
407
+ return true;
408
+ }
409
+ // reset
410
+ else return this.deleteBlockFields(blockId, 'prodOutput', 'prodCtx', 'prodUiCtx', 'prodArgs');
411
+ }
412
+
413
+ /** Optimally sets inputs for multiple blocks in one go */
414
+ public setArgs(requests: SetArgsRequest[]) {
415
+ const changed: string[] = [];
416
+ for (const { blockId, args } of requests) {
417
+ const info = this.getBlockInfo(blockId);
418
+ JSON.parse(args); // checking
419
+ const binary = Buffer.from(args);
420
+ if (Buffer.compare(info.fields.currentArgs!.value!, binary) === 0) continue;
421
+ const argsRef = this.tx.createValue(Pl.JsonObject, binary);
422
+ this.setBlockField(blockId, 'currentArgs', argsRef, 'Ready', binary);
423
+ // will be assigned our author marker
424
+ this.blocksWithChangedInputs.add(blockId);
425
+ changed.push(blockId);
426
+ }
427
+
428
+ // resetting staging outputs for all downstream blocks
429
+ this.getStagingGraph().traverse('downstream', changed, ({ id }) => this.resetStaging(id));
430
+
431
+ if (changed.length > 0) this.updateLastModified();
432
+ }
433
+
434
+ public setUiState(blockId: string, newState: string | undefined): void {
435
+ if (this.blockInfos.get(blockId) === undefined) throw new Error('no such block');
436
+ if (this.blockFrontendStates.get(blockId) === newState) return;
437
+ if (newState === undefined) this.blockFrontendStates.delete(blockId);
438
+ else this.blockFrontendStates.set(blockId, newState);
439
+ this.changedBlockFrontendStates.add(blockId);
440
+ // will be assigned our author marker
441
+ this.blocksWithChangedInputs.add(blockId);
442
+ this.updateLastModified();
443
+ }
444
+
445
+ /** Update block label */
446
+ public setBlockLabel(blockId: string, label: string): void {
447
+ const newStructure = this.structure;
448
+ let ok = false;
449
+ for (const block of allBlocks(newStructure))
450
+ if (block.id === blockId) {
451
+ block.label = label;
452
+ ok = true;
453
+ break;
454
+ }
455
+ if (!ok) throw new Error(`block ${blockId} not found`);
456
+ this.updateStructure(newStructure);
457
+ this.updateLastModified();
458
+ }
459
+
460
+ private createCtx(upstream: Set<string>, ctxField: 'stagingCtx' | 'prodCtx'): AnyRef {
461
+ const upstreamContexts: AnyRef[] = [];
462
+ upstream.forEach((id) => {
463
+ const info = this.getBlockInfo(id);
464
+ if (info.fields[ctxField] === undefined || info.fields[ctxField]!.ref === undefined)
465
+ throw new Error('One of the upstreams staging is not rendered.');
466
+ upstreamContexts.push(Pl.unwrapHolder(this.tx, info.fields[ctxField]!.ref!));
467
+ });
468
+ return createBContextFromUpstreams(this.tx, upstreamContexts);
469
+ }
470
+
471
+ private exportCtx(ctx: AnyRef): AnyRef {
472
+ return exportContext(this.tx, Pl.unwrapHolder(this.tx, this.ctxExportTplHolder), ctx);
473
+ }
474
+
475
+ private renderStagingFor(blockId: string) {
476
+ this.resetStaging(blockId);
477
+
478
+ const info = this.getBlockInfo(blockId);
479
+
480
+ const ctx = this.createCtx(this.getStagingGraph().nodes.get(blockId)!.upstream, 'stagingCtx');
481
+
482
+ if (this.getBlock(blockId).renderingMode !== 'Heavy') throw new Error('not supported yet');
483
+
484
+ const tpl = info.getTemplate(this.tx);
485
+
486
+ const results = createRenderHeavyBlock(this.tx, tpl, {
487
+ args: info.fields.currentArgs!.ref!,
488
+ blockId: this.tx.createValue(Pl.JsonString, JSON.stringify(blockId)),
489
+ isProduction: this.tx.createValue(Pl.JsonBool, JSON.stringify(false)),
490
+ context: ctx
491
+ });
492
+
493
+ this.setBlockField(
494
+ blockId,
495
+ 'stagingCtx',
496
+ Pl.wrapInEphHolder(this.tx, results.context),
497
+ 'NotReady'
498
+ );
499
+
500
+ this.setBlockField(blockId, 'stagingUiCtx', this.exportCtx(results.context), 'NotReady');
501
+
502
+ this.setBlockField(blockId, 'stagingOutput', results.result, 'NotReady');
503
+ }
504
+
505
+ private renderProductionFor(blockId: string) {
506
+ this.resetProduction(blockId);
507
+
508
+ const info = this.getBlockInfo(blockId);
509
+
510
+ const ctx = this.createCtx(
511
+ this.getPendingProductionGraph().nodes.get(blockId)!.upstream,
512
+ 'prodCtx'
513
+ );
514
+
515
+ if (this.getBlock(blockId).renderingMode === 'Light')
516
+ throw new Error("Can't render production for light block.");
517
+
518
+ const tpl = info.getTemplate(this.tx);
519
+
520
+ const results = createRenderHeavyBlock(this.tx, tpl, {
521
+ args: info.fields.currentArgs!.ref!,
522
+ blockId: this.tx.createValue(Pl.JsonString, JSON.stringify(blockId)),
523
+ isProduction: this.tx.createValue(Pl.JsonBool, JSON.stringify(true)),
524
+ context: ctx
525
+ });
526
+ this.setBlockField(
527
+ blockId,
528
+ 'prodCtx',
529
+ Pl.wrapInEphHolder(this.tx, results.context),
530
+ 'NotReady'
531
+ );
532
+ this.setBlockField(blockId, 'prodUiCtx', this.exportCtx(results.context), 'NotReady');
533
+ this.setBlockField(blockId, 'prodOutput', results.result, 'NotReady');
534
+
535
+ // saving inputs for which we rendered the production
536
+ this.setBlockFieldObj(blockId, 'prodArgs', info.fields.currentArgs!);
537
+
538
+ // removing block from limbo as we juts rendered fresh production for it
539
+ if (this.blocksInLimbo.delete(blockId)) this.renderingStateChanged = true;
540
+ }
541
+
542
+ //
543
+ // Structure changes
544
+ //
545
+
546
+ /** Very generic method, better check for more specialized case-specific methods first. */
547
+ public updateStructure(
548
+ newStructure: ProjectStructure,
549
+ newBlockSpecProvider: (blockId: string) => NewBlockSpec = NoNewBlocks
550
+ ): void {
551
+ const currentStagingGraph = this.getStagingGraph();
552
+ const currentActualProductionGraph = this.getActualProductionGraph();
553
+
554
+ const newStagingGraph = stagingGraph(newStructure);
555
+
556
+ // new actual production graph without new blocks
557
+ const newActualProductionGraph = productionGraph(
558
+ newStructure,
559
+ (blockId) => this.blockInfos.get(blockId)?.actualProductionInputs
560
+ );
561
+
562
+ const stagingDiff = graphDiff(currentStagingGraph, newStagingGraph);
563
+ const prodDiff = graphDiff(currentActualProductionGraph, newActualProductionGraph);
564
+
565
+ // removing blocks
566
+ for (const blockId of stagingDiff.onlyInA) {
567
+ const { fields } = this.getBlockInfo(blockId);
568
+ this.deleteBlockFields(blockId, ...(Object.keys(fields) as ProjectField['fieldName'][]));
569
+ this.blockInfos.delete(blockId);
570
+ if (this.blocksInLimbo.delete(blockId)) this.renderingStateChanged = true;
571
+ if (this.blockFrontendStates.delete(blockId)) this.changedBlockFrontendStates.add(blockId);
572
+ }
573
+
574
+ // creating new blocks
575
+ for (const blockId of stagingDiff.onlyInB) {
576
+ const info = new BlockInfo(blockId, {});
577
+ this.blockInfos.set(blockId, info);
578
+ const spec = newBlockSpecProvider(blockId);
579
+
580
+ // block pack
581
+ const bp = createBlockPack(this.tx, spec.blockPack);
582
+ this.setBlockField(blockId, 'blockPack', Pl.wrapInHolder(this.tx, bp), 'NotReady');
583
+
584
+ // args
585
+ const binArgs = Buffer.from(spec.args);
586
+ const argsRes = this.tx.createValue(Pl.JsonObject, binArgs);
587
+ this.setBlockField(blockId, 'currentArgs', argsRes, 'Ready', binArgs);
588
+
589
+ // checking structure
590
+ info.check();
591
+ }
592
+
593
+ // resetting stagings affected by topology change
594
+ for (const blockId of stagingDiff.different) this.resetStaging(blockId);
595
+
596
+ // applying changes due to topology change in production to affected nodes and
597
+ // all their downstreams
598
+ currentActualProductionGraph.traverse('downstream', [...prodDiff.different], (node) => {
599
+ this.resetOrLimboProduction(node.id);
600
+ });
601
+
602
+ if (
603
+ stagingDiff.onlyInB.size > 0 ||
604
+ stagingDiff.onlyInA.size > 0 ||
605
+ stagingDiff.different.size > 0
606
+ )
607
+ this.resetStagingRefreshTimestamp();
608
+
609
+ this.struct = newStructure;
610
+ this.structureChanged = true;
611
+ this.stagingGraph = undefined;
612
+ this.pendingProductionGraph = undefined;
613
+ this.actualProductionGraph = undefined;
614
+
615
+ this.updateLastModified();
616
+ }
617
+
618
+ //
619
+ // Structure change helpers
620
+ //
621
+
622
+ public addBlock(block: Block, spec: NewBlockSpec, before?: string): void {
623
+ const newStruct = this.structure; // copy current structure
624
+ if (before === undefined) {
625
+ // adding as a very last block
626
+ newStruct.groups[newStruct.groups.length - 1].blocks.push(block);
627
+ } else {
628
+ let done = false;
629
+ for (const group of newStruct.groups) {
630
+ const idx = group.blocks.findIndex((b) => b.id === before);
631
+ if (idx < 0) continue;
632
+ group.blocks.splice(idx, 0, block);
633
+ done = true;
634
+ break;
635
+ }
636
+ if (!done) throw new Error(`Can't find element with id: ${before}`);
637
+ }
638
+ this.updateStructure(newStruct, (blockId) => {
639
+ if (blockId !== block.id) throw new Error('Unexpected');
640
+ return spec;
641
+ });
642
+ }
643
+
644
+ public deleteBlock(blockId: string): void {
645
+ const newStruct = this.structure; // copy current structure
646
+ let done = false;
647
+ for (const group of newStruct.groups) {
648
+ const idx = group.blocks.findIndex((b) => b.id === blockId);
649
+ if (idx < 0) continue;
650
+ group.blocks.splice(idx, 1);
651
+ done = true;
652
+ break;
653
+ }
654
+ if (!done) throw new Error(`Can't find element with id: ${blockId}`);
655
+ this.updateStructure(newStruct);
656
+ }
657
+
658
+ //
659
+ // Block-pack migration
660
+ //
661
+
662
+ public migrateBlockPack(blockId: string, spec: BlockPackSpecPrepared, newArgs?: string): void {
663
+ const info = this.getBlockInfo(blockId);
664
+
665
+ this.setBlockField(
666
+ blockId,
667
+ 'blockPack',
668
+ Pl.wrapInHolder(this.tx, createBlockPack(this.tx, spec)),
669
+ 'NotReady'
670
+ );
671
+
672
+ if (newArgs !== undefined) {
673
+ // this will also reset all downstream stagings
674
+ this.setArgs([{ blockId, args: newArgs }]);
675
+ // reset UI state along with args
676
+ this.setUiState(blockId, undefined);
677
+ }
678
+ // resetting staging outputs for all downstream blocks
679
+ else
680
+ this.getStagingGraph().traverse('downstream', [blockId], ({ id }) => this.resetStaging(id));
681
+
682
+ // also reset or limbo all downstream productions
683
+ if (info.productionRendered)
684
+ this.getActualProductionGraph().traverse('downstream', [blockId], ({ id }) =>
685
+ this.resetOrLimboProduction(id)
686
+ );
687
+
688
+ this.updateLastModified();
689
+ }
690
+
691
+ //
692
+ // Render
693
+ //
694
+
695
+ public renderProduction(blockIds: string[], addUpstreams: boolean = false): Set<string> {
696
+ const blockIdsSet = new Set(blockIds);
697
+
698
+ const prodGraph = this.getPendingProductionGraph();
699
+ if (addUpstreams)
700
+ // adding all upstreams automatically
701
+ prodGraph.traverse('upstream', blockIds, (node) => {
702
+ blockIdsSet.add(node.id);
703
+ });
704
+ // checking that targets contain all upstreams
705
+ else
706
+ for (const blockId of blockIdsSet) {
707
+ const node = prodGraph.nodes.get(blockId);
708
+ if (node === undefined) throw new Error(`Can't find block with id: ${blockId}`);
709
+ for (const upstream of node.upstream)
710
+ if (!blockIdsSet.has(upstream))
711
+ throw new Error("Can't render blocks not including all upstreams.");
712
+ }
713
+
714
+ // traversing in topological order and rendering target blocks
715
+ const rendered = new Set<string>();
716
+ for (const block of allBlocks(this.structure)) {
717
+ if (!blockIdsSet.has(block.id)) continue;
718
+
719
+ let render =
720
+ this.getBlockInfo(block.id).requireProductionRendering || this.blocksInLimbo.has(block.id);
721
+
722
+ if (!render)
723
+ for (const upstream of prodGraph.nodes.get(block.id)!.upstream)
724
+ if (rendered.has(upstream)) {
725
+ render = true;
726
+ break;
727
+ }
728
+
729
+ if (render) {
730
+ this.renderProductionFor(block.id);
731
+ rendered.add(block.id);
732
+ }
733
+ }
734
+
735
+ // sending to limbo all downstream blocks
736
+ prodGraph.traverse('downstream', [...rendered], (node) => {
737
+ if (rendered.has(node.id))
738
+ // don't send to limbo blocks that were just rendered
739
+ return;
740
+ this.resetOrLimboProduction(node.id);
741
+ });
742
+
743
+ if (rendered.size > 0) this.updateLastModified();
744
+
745
+ return rendered;
746
+ }
747
+
748
+ /** Stops running blocks from the list and modify states of other blocks
749
+ * accordingly */
750
+ public stopProduction(...blockIds: string[]) {
751
+ const activeProdGraph = this.getActualProductionGraph();
752
+
753
+ // we will stop all blocks listed in request and all their downstreams
754
+ const queue = new Denque(blockIds);
755
+ const queued = new Set(blockIds);
756
+ const stopped: string[] = [];
757
+
758
+ while (!queue.isEmpty()) {
759
+ const blockId = queue.shift()!;
760
+ const fields = this.getBlockInfo(blockId).fields;
761
+
762
+ if (fields.prodOutput?.status === 'Ready' && fields.prodCtx?.status === 'Ready')
763
+ // skipping finished blocks
764
+ continue;
765
+
766
+ if (this.deleteBlockFields(blockId, 'prodOutput', 'prodCtx', 'prodUiCtx', 'prodArgs')) {
767
+ // was actually stopped
768
+ stopped.push(blockId);
769
+
770
+ // will try to stop all its downstreams
771
+ for (const downstream of activeProdGraph.traverseIdsExcludingRoots('downstream', blockId)) {
772
+ if (queued.has(downstream)) continue;
773
+ queue.push(downstream);
774
+ queued.add(downstream);
775
+ }
776
+ }
777
+ }
778
+
779
+ // blocks under stopped blocks, but still having results, goes to limbo
780
+ for (const blockId of activeProdGraph.traverseIdsExcludingRoots('downstream', ...stopped))
781
+ this.resetOrLimboProduction(blockId);
782
+ }
783
+
784
+ private traverseWithStagingLag(cb: (blockId: string, lag: number) => void) {
785
+ const lags = new Map<string, number>();
786
+ const stagingGraph = this.getStagingGraph();
787
+ stagingGraph.nodes.forEach((node) => {
788
+ const info = this.getBlockInfo(node.id);
789
+ let lag = info.stagingRendered ? 0 : 1;
790
+ node.upstream.forEach((upstream) => {
791
+ const upstreamLag = lags.get(upstream)!;
792
+ if (upstreamLag === 0) return;
793
+ lag = Math.max(upstreamLag + 1, lag);
794
+ });
795
+ cb(node.id, lag);
796
+ lags.set(node.id, lag);
797
+ });
798
+ }
799
+
800
+ /** @param stagingRenderingRate rate in blocks per second */
801
+ private refreshStagings(stagingRenderingRate?: number) {
802
+ const elapsed = Date.now() - this.renderingState.stagingRefreshTimestamp;
803
+ const lagThreshold =
804
+ stagingRenderingRate === undefined
805
+ ? undefined
806
+ : 1 + Math.max(0, (elapsed * stagingRenderingRate) / 1000);
807
+ let rendered = 0;
808
+ this.traverseWithStagingLag((blockId, lag) => {
809
+ if (lag === 0)
810
+ // meaning staging already rendered
811
+ return;
812
+ if (lagThreshold === undefined || lag <= lagThreshold) {
813
+ this.renderStagingFor(blockId);
814
+ rendered++;
815
+ }
816
+ });
817
+ if (rendered > 0) this.resetStagingRefreshTimestamp();
818
+ }
819
+
820
+ //
821
+ // Meta
822
+ //
823
+
824
+ /** Updates project metadata */
825
+ public setMeta(meta: ProjectMeta): void {
826
+ this.meta = meta;
827
+ this.metaChanged = true;
828
+ this.updateLastModified();
829
+ }
830
+
831
+ //
832
+ // Maintenance
833
+ //
834
+
835
+ /** @param stagingRenderingRate rate in blocks per second */
836
+ public doRefresh(stagingRenderingRate?: number) {
837
+ this.refreshStagings(stagingRenderingRate);
838
+ this.blockInfos.forEach((blockInfo) => {
839
+ if (
840
+ blockInfo.fields.prodCtx?.status === 'Ready' &&
841
+ blockInfo.fields.prodOutput?.status === 'Ready'
842
+ )
843
+ this.deleteBlockFields(
844
+ blockInfo.id,
845
+ 'prodOutputPrevious',
846
+ 'prodCtxPrevious',
847
+ 'prodUiCtxPrevious'
848
+ );
849
+ if (
850
+ blockInfo.fields.stagingCtx?.status === 'Ready' &&
851
+ blockInfo.fields.stagingOutput?.status === 'Ready'
852
+ )
853
+ this.deleteBlockFields(
854
+ blockInfo.id,
855
+ 'stagingOutputPrevious',
856
+ 'stagingCtxPrevious',
857
+ 'stagingUiCtxPrevious'
858
+ );
859
+ });
860
+ }
861
+
862
+ private assignAuthorMarkers() {
863
+ const markerStr = this.author ? JSON.stringify(this.author) : undefined;
864
+
865
+ for (const blockId of this.blocksWithChangedInputs)
866
+ if (markerStr === undefined) this.tx.deleteKValue(this.rid, blockArgsAuthorKey(blockId));
867
+ else this.tx.setKValue(this.rid, blockArgsAuthorKey(blockId), markerStr);
868
+
869
+ if (this.metaChanged || this.structureChanged) {
870
+ if (markerStr === undefined) this.tx.deleteKValue(this.rid, ProjectStructureAuthorKey);
871
+ else this.tx.setKValue(this.rid, ProjectStructureAuthorKey, markerStr);
872
+ }
873
+ }
874
+
875
+ public save() {
876
+ if (!this.wasModified) return;
877
+
878
+ if (this.lastModifiedChanged)
879
+ this.tx.setKValue(this.rid, ProjectLastModifiedTimestamp, JSON.stringify(this.lastModified));
880
+
881
+ if (this.structureChanged)
882
+ this.tx.setKValue(this.rid, ProjectStructureKey, JSON.stringify(this.struct));
883
+
884
+ if (this.renderingStateChanged)
885
+ this.tx.setKValue(
886
+ this.rid,
887
+ BlockRenderingStateKey,
888
+ JSON.stringify({
889
+ ...this.renderingState,
890
+ blocksInLimbo: [...this.blocksInLimbo]
891
+ } as ProjectRenderingState)
892
+ );
893
+
894
+ if (this.metaChanged) this.tx.setKValue(this.rid, ProjectMetaKey, JSON.stringify(this.meta));
895
+
896
+ for (const blockId of this.changedBlockFrontendStates) {
897
+ const uiState = this.blockFrontendStates.get(blockId);
898
+ if (uiState === undefined) this.tx.deleteKValue(this.rid, blockFrontendStateKey(blockId));
899
+ else this.tx.setKValue(this.rid, blockFrontendStateKey(blockId), uiState);
900
+ }
901
+
902
+ this.assignAuthorMarkers();
903
+ }
904
+
905
+ public static async load(
906
+ tx: PlTransaction,
907
+ rid: ResourceId,
908
+ author?: AuthorMarker
909
+ ): Promise<ProjectMutator> {
910
+ const fullResourceStateP = tx.getResourceData(rid, true);
911
+ const schemaP = tx.getKValueJson<string>(rid, SchemaVersionKey);
912
+ const lastModifiedP = tx.getKValueJson<number>(rid, ProjectLastModifiedTimestamp);
913
+ const metaP = tx.getKValueJson<ProjectMeta>(rid, ProjectMetaKey);
914
+ const structureP = tx.getKValueJson<ProjectStructure>(rid, ProjectStructureKey);
915
+ const renderingStateP = tx.getKValueJson<ProjectRenderingState>(rid, BlockRenderingStateKey);
916
+
917
+ const allKVP = tx.listKeyValuesString(rid);
918
+
919
+ // loading jsons
920
+ const [
921
+ fullResourceState,
922
+ schema,
923
+ lastModified,
924
+ meta,
925
+ structure,
926
+ { stagingRefreshTimestamp, blocksInLimbo },
927
+ allKV
928
+ ] = await Promise.all([
929
+ fullResourceStateP,
930
+ schemaP,
931
+ lastModifiedP,
932
+ metaP,
933
+ structureP,
934
+ renderingStateP,
935
+ allKVP
936
+ ]);
937
+ if (schema !== SchemaVersionCurrent)
938
+ throw new Error(
939
+ `Can't act on this project resource because it has a wrong schema version: ${schema}`
940
+ );
941
+
942
+ // loading field information
943
+ const blockInfoStates = new Map<string, BlockInfoState>();
944
+ for (const f of fullResourceState.fields) {
945
+ const projectField = parseProjectField(f.name);
946
+
947
+ // processing only fields with known structure
948
+ if (projectField === undefined) continue;
949
+
950
+ let info = blockInfoStates.get(projectField.blockId);
951
+ if (info === undefined) {
952
+ info = {
953
+ id: projectField.blockId,
954
+ fields: {}
955
+ };
956
+ blockInfoStates.set(projectField.blockId, info);
957
+ }
958
+
959
+ info.fields[projectField.fieldName] = isNullResourceId(f.value)
960
+ ? { modCount: 0 }
961
+ : { modCount: 0, ref: f.value };
962
+ }
963
+
964
+ // loading ctx export template to check if we already have cached materialized template in our project
965
+ const ctxExportTplEnvelope = await getPreparedExportTemplateEnvelope();
966
+
967
+ // expected field name
968
+ const ctxExportTplCacheFieldName = getServiceTemplateField(ctxExportTplEnvelope.hash);
969
+ const ctxExportTplField = fullResourceState.fields.find(
970
+ (f) => f.name === ctxExportTplCacheFieldName
971
+ );
972
+ let ctxExportTplHolder: AnyResourceRef;
973
+ if (ctxExportTplField !== undefined)
974
+ ctxExportTplHolder = ensureResourceIdNotNull(ctxExportTplField.value);
975
+ else {
976
+ ctxExportTplHolder = Pl.wrapInHolder(tx, loadTemplate(tx, ctxExportTplEnvelope.spec));
977
+ tx.createField(
978
+ field(rid, getServiceTemplateField(ctxExportTplEnvelope.hash)),
979
+ 'Dynamic',
980
+ ctxExportTplHolder
981
+ );
982
+ }
983
+
984
+ const renderingState = { stagingRefreshTimestamp };
985
+ const blocksInLimboSet = new Set(blocksInLimbo);
986
+
987
+ const blockFrontendStates = new Map<string, string>();
988
+ for (const kv of allKV) {
989
+ const blockId = parseBlockFrontendStateKey(kv.key);
990
+ if (blockId === undefined) continue;
991
+ blockFrontendStates.set(blockId, kv.value);
992
+ }
993
+
994
+ const requests: [BlockFieldState, Promise<BasicResourceData>][] = [];
995
+ blockInfoStates!.forEach(({ id, fields }) => {
996
+ for (const [, state] of Object.entries(fields)) {
997
+ if (state.ref === undefined) continue;
998
+ if (!isResource(state.ref) || isResourceRef(state.ref))
999
+ throw new Error('unexpected behaviour');
1000
+ requests.push([state, tx.getResourceData(state.ref, false)]);
1001
+ }
1002
+ });
1003
+ for (const [state, response] of requests) {
1004
+ const result = await response;
1005
+ state.value = result.data;
1006
+ if (isNotNullResourceId(result.error)) state.status = 'Error';
1007
+ else if (result.resourceReady || isNotNullResourceId(result.originalResourceId))
1008
+ state.status = 'Ready';
1009
+ else state.status = 'NotReady';
1010
+ }
1011
+
1012
+ const blockInfos = new Map<string, BlockInfo>();
1013
+ blockInfoStates.forEach(({ id, fields }) => blockInfos.set(id, new BlockInfo(id, fields)));
1014
+
1015
+ // check consistency of project state
1016
+ const blockInStruct = new Set<string>();
1017
+ for (const b of allBlocks(structure)) {
1018
+ if (!blockInfos.has(b.id))
1019
+ throw new Error(`Inconsistent project structure: no inputs for ${b.id}`);
1020
+ blockInStruct.add(b.id);
1021
+ }
1022
+ blockInfos.forEach((info) => {
1023
+ if (!blockInStruct.has(info.id))
1024
+ throw new Error(`Inconsistent project structure: no structure entry for ${info.id}`);
1025
+ // checking structure
1026
+ info.check();
1027
+ });
1028
+
1029
+ const prj = new ProjectMutator(
1030
+ rid,
1031
+ tx,
1032
+ author,
1033
+ schema,
1034
+ lastModified,
1035
+ meta,
1036
+ structure,
1037
+ renderingState,
1038
+ blocksInLimboSet,
1039
+ blockInfos,
1040
+ blockFrontendStates,
1041
+ ctxExportTplHolder
1042
+ );
1043
+
1044
+ prj.fixProblems();
1045
+
1046
+ return prj;
1047
+ }
1048
+ }
1049
+
1050
+ export interface ProjectState {
1051
+ schema: string;
1052
+ structure: ProjectStructure;
1053
+ renderingState: Omit<ProjectRenderingState, 'blocksInLimbo'>;
1054
+ blocksInLimbo: Set<string>;
1055
+ blockInfos: Map<string, BlockInfo>;
1056
+ }
1057
+
1058
+ export async function createProject(
1059
+ tx: PlTransaction,
1060
+ meta: ProjectMeta = InitialBlockMeta
1061
+ ): Promise<AnyResourceRef> {
1062
+ const prj = tx.createEphemeral(ProjectResourceType);
1063
+ tx.lock(prj);
1064
+ const ts = String(Date.now());
1065
+ tx.setKValue(prj, SchemaVersionKey, JSON.stringify(SchemaVersionCurrent));
1066
+ tx.setKValue(prj, ProjectCreatedTimestamp, ts);
1067
+ tx.setKValue(prj, ProjectLastModifiedTimestamp, ts);
1068
+ tx.setKValue(prj, ProjectMetaKey, JSON.stringify(meta));
1069
+ tx.setKValue(prj, ProjectStructureKey, JSON.stringify(InitialBlockStructure));
1070
+ tx.setKValue(prj, BlockRenderingStateKey, JSON.stringify(InitialProjectRenderingState));
1071
+ const ctxExportTplEnvelope = await getPreparedExportTemplateEnvelope();
1072
+ tx.createField(
1073
+ field(prj, getServiceTemplateField(ctxExportTplEnvelope.hash)),
1074
+ 'Dynamic',
1075
+ Pl.wrapInHolder(tx, loadTemplate(tx, ctxExportTplEnvelope.spec))
1076
+ );
1077
+ return prj;
1078
+ }
1079
+
1080
+ export async function withProject<T>(
1081
+ txOrPl: PlTransaction | PlClient,
1082
+ rid: ResourceId,
1083
+ cb: (p: ProjectMutator) => T | Promise<T>
1084
+ ): Promise<T> {
1085
+ return withProjectAuthored(txOrPl, rid, undefined, cb);
1086
+ }
1087
+
1088
+ export async function withProjectAuthored<T>(
1089
+ txOrPl: PlTransaction | PlClient,
1090
+ rid: ResourceId,
1091
+ author: AuthorMarker | undefined,
1092
+ cb: (p: ProjectMutator) => T | Promise<T>
1093
+ ): Promise<T> {
1094
+ if (txOrPl instanceof PlClient) {
1095
+ return await txOrPl.withWriteTx('ProjectAction', async (tx) => {
1096
+ const mut = await ProjectMutator.load(tx, rid, author);
1097
+ const result = await cb(mut);
1098
+ if (!mut.wasModified)
1099
+ // skipping save and commit altogether if no modifications were
1100
+ // actually made
1101
+ return result;
1102
+ mut.save();
1103
+ await tx.commit();
1104
+ return result;
1105
+ });
1106
+ } else {
1107
+ const mut = await ProjectMutator.load(txOrPl, rid, author);
1108
+ const result = await cb(mut);
1109
+ mut.save();
1110
+ return result;
1111
+ }
1112
+ }