@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.
- package/dist/block_registry/index.d.ts +4 -0
- package/dist/block_registry/index.d.ts.map +1 -0
- package/dist/block_registry/registry.d.ts +37 -0
- package/dist/block_registry/registry.d.ts.map +1 -0
- package/dist/block_registry/registry_spec.d.ts +12 -0
- package/dist/block_registry/registry_spec.d.ts.map +1 -0
- package/dist/block_registry/watcher.d.ts +15 -0
- package/dist/block_registry/watcher.d.ts.map +1 -0
- package/dist/block_registry/well_known_registries.d.ts +4 -0
- package/dist/block_registry/well_known_registries.d.ts.map +1 -0
- package/dist/cfg_render/executor.d.ts +8 -0
- package/dist/cfg_render/executor.d.ts.map +1 -0
- package/dist/cfg_render/operation.d.ts +29 -0
- package/dist/cfg_render/operation.d.ts.map +1 -0
- package/dist/cfg_render/renderer.d.ts +6 -0
- package/dist/cfg_render/renderer.d.ts.map +1 -0
- package/dist/cfg_render/traverse.d.ts +3 -0
- package/dist/cfg_render/traverse.d.ts.map +1 -0
- package/dist/cfg_render/util.d.ts +5 -0
- package/dist/cfg_render/util.d.ts.map +1 -0
- package/dist/dev/index.d.ts +21 -0
- package/dist/dev/index.d.ts.map +1 -0
- package/dist/dev/util.d.ts +3 -0
- package/dist/dev/util.d.ts.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3587 -0
- package/dist/index.mjs.map +1 -0
- package/dist/js_render/context.d.ts +68 -0
- package/dist/js_render/context.d.ts.map +1 -0
- package/dist/js_render/index.d.ts +6 -0
- package/dist/js_render/index.d.ts.map +1 -0
- package/dist/middle_layer/active_cfg.d.ts +6 -0
- package/dist/middle_layer/active_cfg.d.ts.map +1 -0
- package/dist/middle_layer/block.d.ts +9 -0
- package/dist/middle_layer/block.d.ts.map +1 -0
- package/dist/middle_layer/block_ctx.d.ts +20 -0
- package/dist/middle_layer/block_ctx.d.ts.map +1 -0
- package/dist/middle_layer/block_ctx_unsafe.d.ts +16 -0
- package/dist/middle_layer/block_ctx_unsafe.d.ts.map +1 -0
- package/dist/middle_layer/driver_kit.d.ts +31 -0
- package/dist/middle_layer/driver_kit.d.ts.map +1 -0
- package/dist/middle_layer/frontend_path.d.ts +6 -0
- package/dist/middle_layer/frontend_path.d.ts.map +1 -0
- package/dist/middle_layer/index.d.ts +5 -0
- package/dist/middle_layer/index.d.ts.map +1 -0
- package/dist/middle_layer/middle_layer.d.ts +78 -0
- package/dist/middle_layer/middle_layer.d.ts.map +1 -0
- package/dist/middle_layer/navigation_states.d.ts +10 -0
- package/dist/middle_layer/navigation_states.d.ts.map +1 -0
- package/dist/middle_layer/ops.d.ts +64 -0
- package/dist/middle_layer/ops.d.ts.map +1 -0
- package/dist/middle_layer/project.d.ts +110 -0
- package/dist/middle_layer/project.d.ts.map +1 -0
- package/dist/middle_layer/project_list.d.ts +11 -0
- package/dist/middle_layer/project_list.d.ts.map +1 -0
- package/dist/middle_layer/project_overview.d.ts +8 -0
- package/dist/middle_layer/project_overview.d.ts.map +1 -0
- package/dist/middle_layer/render.d.ts +6 -0
- package/dist/middle_layer/render.d.ts.map +1 -0
- package/dist/middle_layer/types.d.ts +11 -0
- package/dist/middle_layer/types.d.ts.map +1 -0
- package/dist/middle_layer/util.d.ts +3 -0
- package/dist/middle_layer/util.d.ts.map +1 -0
- package/dist/model/args.d.ts +12 -0
- package/dist/model/args.d.ts.map +1 -0
- package/dist/model/block_pack.d.ts +8 -0
- package/dist/model/block_pack.d.ts.map +1 -0
- package/dist/model/block_pack_spec.d.ts +40 -0
- package/dist/model/block_pack_spec.d.ts.map +1 -0
- package/dist/model/frontend.d.ts +10 -0
- package/dist/model/frontend.d.ts.map +1 -0
- package/dist/model/index.d.ts +3 -0
- package/dist/model/index.d.ts.map +1 -0
- package/dist/model/project_model.d.ts +67 -0
- package/dist/model/project_model.d.ts.map +1 -0
- package/dist/model/project_model_util.d.ts +29 -0
- package/dist/model/project_model_util.d.ts.map +1 -0
- package/dist/model/template_spec.d.ts +16 -0
- package/dist/model/template_spec.d.ts.map +1 -0
- package/dist/mutator/block-pack/block_pack.d.ts +17 -0
- package/dist/mutator/block-pack/block_pack.d.ts.map +1 -0
- package/dist/mutator/block-pack/frontend.d.ts +4 -0
- package/dist/mutator/block-pack/frontend.d.ts.map +1 -0
- package/dist/mutator/context_export.d.ts +9 -0
- package/dist/mutator/context_export.d.ts.map +1 -0
- package/dist/mutator/project.d.ts +121 -0
- package/dist/mutator/project.d.ts.map +1 -0
- package/dist/mutator/template/render_block.d.ts +32 -0
- package/dist/mutator/template/render_block.d.ts.map +1 -0
- package/dist/mutator/template/render_template.d.ts +12 -0
- package/dist/mutator/template/render_template.d.ts.map +1 -0
- package/dist/mutator/template/template_loading.d.ts +13 -0
- package/dist/mutator/template/template_loading.d.ts.map +1 -0
- package/dist/pool/data.d.ts +24 -0
- package/dist/pool/data.d.ts.map +1 -0
- package/dist/pool/driver.d.ts +22 -0
- package/dist/pool/driver.d.ts.map +1 -0
- package/dist/pool/index.d.ts +3 -0
- package/dist/pool/index.d.ts.map +1 -0
- package/dist/pool/p_object_collection.d.ts +29 -0
- package/dist/pool/p_object_collection.d.ts.map +1 -0
- package/dist/pool/ref_count_pool.d.ts +25 -0
- package/dist/pool/ref_count_pool.d.ts.map +1 -0
- package/dist/pool/result_pool.d.ts +25 -0
- package/dist/pool/result_pool.d.ts.map +1 -0
- package/dist/test/block_packs.d.ts +6 -0
- package/dist/test/block_packs.d.ts.map +1 -0
- package/dist/test/explicit_templates.d.ts +3 -0
- package/dist/test/explicit_templates.d.ts.map +1 -0
- package/dist/test/known_templates.d.ts +6 -0
- package/dist/test/known_templates.d.ts.map +1 -0
- package/package.json +55 -0
- package/src/block_registry/index.ts +3 -0
- package/src/block_registry/registry.test.ts +35 -0
- package/src/block_registry/registry.ts +180 -0
- package/src/block_registry/registry_spec.ts +13 -0
- package/src/block_registry/watcher.ts +72 -0
- package/src/block_registry/well_known_registries.ts +13 -0
- package/src/cfg_render/executor.test.ts +120 -0
- package/src/cfg_render/executor.ts +253 -0
- package/src/cfg_render/operation.ts +38 -0
- package/src/cfg_render/renderer.ts +540 -0
- package/src/cfg_render/traverse.ts +58 -0
- package/src/cfg_render/util.ts +29 -0
- package/src/dev/index.ts +89 -0
- package/src/dev/util.ts +13 -0
- package/src/index.ts +21 -0
- package/src/js_render/context.ts +768 -0
- package/src/js_render/index.ts +41 -0
- package/src/middle_layer/active_cfg.ts +56 -0
- package/src/middle_layer/block.ts +70 -0
- package/src/middle_layer/block_ctx.ts +90 -0
- package/src/middle_layer/block_ctx_unsafe.ts +29 -0
- package/src/middle_layer/driver_kit.ts +107 -0
- package/src/middle_layer/frontend_path.ts +83 -0
- package/src/middle_layer/index.ts +4 -0
- package/src/middle_layer/middle_layer.test.ts +720 -0
- package/src/middle_layer/middle_layer.ts +235 -0
- package/src/middle_layer/navigation_states.ts +48 -0
- package/src/middle_layer/ops.ts +147 -0
- package/src/middle_layer/project.ts +380 -0
- package/src/middle_layer/project_list.ts +59 -0
- package/src/middle_layer/project_overview.ts +220 -0
- package/src/middle_layer/render.test.ts +129 -0
- package/src/middle_layer/render.ts +19 -0
- package/src/middle_layer/types.ts +16 -0
- package/src/middle_layer/util.ts +22 -0
- package/src/model/args.ts +62 -0
- package/src/model/block_pack.ts +8 -0
- package/src/model/block_pack_spec.ts +52 -0
- package/src/model/frontend.ts +10 -0
- package/src/model/index.ts +2 -0
- package/src/model/project_model.test.ts +26 -0
- package/src/model/project_model.ts +142 -0
- package/src/model/project_model_util.test.ts +88 -0
- package/src/model/project_model_util.ts +169 -0
- package/src/model/template_spec.ts +18 -0
- package/src/mutator/block-pack/block_pack.test.ts +53 -0
- package/src/mutator/block-pack/block_pack.ts +187 -0
- package/src/mutator/block-pack/frontend.ts +29 -0
- package/src/mutator/context_export.ts +25 -0
- package/src/mutator/project.test.ts +272 -0
- package/src/mutator/project.ts +1112 -0
- package/src/mutator/template/render_block.ts +91 -0
- package/src/mutator/template/render_template.ts +40 -0
- package/src/mutator/template/template_loading.ts +77 -0
- package/src/mutator/template/template_render.test.ts +272 -0
- package/src/pool/data.ts +239 -0
- package/src/pool/driver.ts +325 -0
- package/src/pool/index.ts +2 -0
- package/src/pool/p_object_collection.ts +122 -0
- package/src/pool/ref_count_pool.ts +76 -0
- package/src/pool/result_pool.ts +284 -0
- package/src/test/block_packs.ts +23 -0
- package/src/test/explicit_templates.ts +8 -0
- 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
|
+
}
|