@platforma-sdk/workflow-tengo 4.6.5 → 4.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/tengo/lib/exec.constants.lib.tengo +6 -2
  2. package/dist/tengo/lib/exec.lib.tengo +9 -6
  3. package/dist/tengo/lib/exec.limits.lib.tengo +38 -13
  4. package/dist/tengo/lib/exec.runcmd.lib.tengo +35 -23
  5. package/dist/tengo/lib/feats.lib.tengo +17 -1
  6. package/dist/tengo/lib/smart.lib.tengo +16 -0
  7. package/dist/tengo/lib/tpl.base.lib.tengo +6 -2
  8. package/dist/tengo/lib/tpl.state-await.lib.tengo +88 -29
  9. package/dist/tengo/tpl/check_network.create_workdir_from_storage.plj.gz +0 -0
  10. package/dist/tengo/tpl/check_network.download_blob.plj.gz +0 -0
  11. package/dist/tengo/tpl/check_network.run_hello_world.plj.gz +0 -0
  12. package/dist/tengo/tpl/check_network.run_hello_world_py.plj.gz +0 -0
  13. package/dist/tengo/tpl/check_network.upload_blob.plj.gz +0 -0
  14. package/dist/tengo/tpl/check_network.upload_template.plj.gz +0 -0
  15. package/dist/tengo/tpl/exec.R.create-renv.plj.gz +0 -0
  16. package/dist/tengo/tpl/exec.exec.plj.gz +0 -0
  17. package/dist/tengo/tpl/exec.monetization_info.plj.gz +0 -0
  18. package/dist/tengo/tpl/exec.monetization_to_json.plj.gz +0 -0
  19. package/dist/tengo/tpl/exec.python.create-venv.plj.gz +0 -0
  20. package/dist/tengo/tpl/exec.python.install-deps.plj.gz +0 -0
  21. package/dist/tengo/tpl/exec.python.venv-bin-path.plj.gz +0 -0
  22. package/dist/tengo/tpl/ll.get-field.plj.gz +0 -0
  23. package/dist/tengo/tpl/pframes.export-multiple-pcolumns.plj.gz +0 -0
  24. package/dist/tengo/tpl/pframes.export-pframe-for-ui.plj.gz +0 -0
  25. package/dist/tengo/tpl/pframes.export-pframe.plj.gz +0 -0
  26. package/dist/tengo/tpl/pframes.export-single-pcolumn.plj.gz +0 -0
  27. package/dist/tengo/tpl/pframes.import-dir.plj.gz +0 -0
  28. package/dist/tengo/tpl/pframes.process-pcolumn-data.plj.gz +0 -0
  29. package/dist/tengo/tpl/pframes.remove-nulls.plj.gz +0 -0
  30. package/dist/tengo/tpl/pframes.slice-data.plj.gz +0 -0
  31. package/dist/tengo/tpl/pframes.xsv-export-pframe.plj.gz +0 -0
  32. package/dist/tengo/tpl/pframes.xsv-import-if-not-null.plj.gz +0 -0
  33. package/dist/tengo/tpl/pframes.xsv-import-map.plj.gz +0 -0
  34. package/dist/tengo/tpl/waiter.waiter.plj.gz +0 -0
  35. package/dist/tengo/tpl/workdir.save.plj.gz +0 -0
  36. package/dist/tengo/tpl/workflow.build-ctx.plj.gz +0 -0
  37. package/dist/tengo/tpl/workflow.query-anchored.plj.gz +0 -0
  38. package/package.json +1 -1
@@ -1,21 +1,25 @@
1
+ ll := import("@platforma-sdk/workflow-tengo:ll")
2
+
1
3
  ARG_TYPE_STRING := "string"
2
4
  ARG_TYPE_EXPRESSION := "expressionRef"
3
5
  ARG_TYPE_VAR := "variableRef" // TODO: deprecated, remove after 15 Mar 2025
4
6
  ARG_TYPES := [ARG_TYPE_STRING, ARG_TYPE_EXPRESSION, ARG_TYPE_VAR]
5
7
 
8
+ BATCH_QUEUE := "batch" // special queue in storage controller with no limits
6
9
  HEAVY_QUEUE := "heavy"
7
10
  MEDIUM_QUEUE := "medium"
8
11
  LIGHT_QUEUE := "light"
9
12
  UI_TASKS_QUEUE := "ui-tasks"
10
13
 
11
- export {
14
+ export ll.toStrict({
12
15
  ARG_TYPE_STRING: ARG_TYPE_STRING,
13
16
  ARG_TYPE_EXPRESSION: ARG_TYPE_EXPRESSION,
14
17
  ARG_TYPE_VAR: ARG_TYPE_VAR,
15
18
  ARG_TYPES: ARG_TYPES,
16
19
 
20
+ BATCH_QUEUE: BATCH_QUEUE,
17
21
  UI_TASKS_QUEUE: UI_TASKS_QUEUE,
18
22
  LIGHT_QUEUE: LIGHT_QUEUE,
19
23
  MEDIUM_QUEUE: MEDIUM_QUEUE,
20
24
  HEAVY_QUEUE: HEAVY_QUEUE
21
- }
25
+ })
@@ -759,12 +759,15 @@ builder := func() {
759
759
  }
760
760
 
761
761
  serviceExecInputs := {}
762
- if feats.workdirLimits || feats.computeLimits {
763
- serviceExecInputs.quota = limits.quotaBuilder().
764
- queue(queue).
765
- cpu(cpuRequest). // 'undefined' value is OK and makes runner to apply default queue limits
766
- ram(ramRequest). // 'undefined' value is OK and makes runner to apply default queue limits
767
- build()
762
+ if feats.workdirLimits || feats.computeLimits || limits.useBatch(queue) {
763
+ if (is_int(ramRequest)) {
764
+ ramRequest = string(ramRequest) + "b"
765
+ }
766
+ serviceExecInputs.quota = {
767
+ queue: queue,
768
+ cpu: cpuRequest,
769
+ ram: ramRequest
770
+ }
768
771
  }
769
772
 
770
773
 
@@ -9,8 +9,11 @@ ll := import("@platforma-sdk/workflow-tengo:ll")
9
9
 
10
10
  oop := import("@platforma-sdk/workflow-tengo:oop")
11
11
  validation := import("@platforma-sdk/workflow-tengo:validation")
12
+ slices := import("@platforma-sdk/workflow-tengo:slices")
12
13
  smart := import("@platforma-sdk/workflow-tengo:smart")
13
14
  json := import("json")
15
+ constants := import("@platforma-sdk/workflow-tengo:exec.constants")
16
+ feats := import("@platforma-sdk/workflow-tengo:feats")
14
17
 
15
18
  _RTYPE_QUOTA := { Name: "Quota", Version: "1" }
16
19
 
@@ -19,6 +22,16 @@ _RTYPE_QUOTA := { Name: "Quota", Version: "1" }
19
22
 
20
23
 
21
24
 
25
+
26
+ useBatch := func(queueName) {
27
+ return feats.hasBatch && !slices.hasElement([ constants.UI_TASKS_QUEUE ], queueName)
28
+ }
29
+
30
+
31
+
32
+
33
+
34
+
22
35
  quotaBuilder := func() {
23
36
  self := undefined
24
37
 
@@ -132,9 +145,11 @@ storageSpaceRequestBuilder := func() {
132
145
  requestBuilder := smart.ephemeralBuilder(_RTYPE_STORAGE_SPACE_REQUEST)
133
146
  requestBuilder.getField(_SSR_FIELD_REQUEST).set(quota)
134
147
 
148
+ requestRef := requestBuilder.lockAndBuild()
149
+
135
150
  return ll.toStrict(
136
- oop.inherit(requestBuilder, {
137
- allocation: requestBuilder.getField(_SSR_FIELD_ALLOCATION)
151
+ oop.inherit(requestRef, {
152
+ allocation: requestRef.getField(_SSR_FIELD_ALLOCATION)
138
153
  })
139
154
  )
140
155
  }
@@ -143,8 +158,9 @@ storageSpaceRequestBuilder := func() {
143
158
  return self
144
159
  }
145
160
 
146
- _RTYPE_COMPUTE_REQUEST := func(executor) { return { Name: "ComputeRequest/" + executor, Version: "1" } }
147
- _CR_DEFAULT_EXECUTOR_ID := "executor"
161
+ _RTYPE_COMPUTE_REQUEST_EXECUTOR_V1 := { Name: "ComputeRequest/executor", Version: "1" }
162
+ _RTYPE_COMPUTE_REQUEST_BATCH_V1 := { Name: "ComputeRequest/batch", Version: "1" }
163
+
148
164
  _CR_FIELD_REQUEST := "request" // input
149
165
  _CR_FIELD_ALLOCATION := "allocation" // output
150
166
 
@@ -152,7 +168,7 @@ computeRequestBuilder := func() {
152
168
  self := undefined
153
169
 
154
170
  quota := undefined
155
- executor := _CR_DEFAULT_EXECUTOR_ID
171
+ queue_name := undefined
156
172
 
157
173
  self = ll.toStrict({
158
174
 
@@ -173,10 +189,9 @@ computeRequestBuilder := func() {
173
189
 
174
190
 
175
191
 
176
-
177
- executor: func(execID) {
178
- validation.assertType(execID, "string", "exec.limits.executor: executor ID should be a string")
179
- executor = execID
192
+ queue: func(name) {
193
+ validation.assertType(name, "string", "exec.limits.queue: queue name should be a string")
194
+ queue_name = name
180
195
 
181
196
  return self
182
197
  },
@@ -188,14 +203,23 @@ computeRequestBuilder := func() {
188
203
 
189
204
  build: func() {
190
205
  ll.assert(smart.isReference(quota), "exec.limits.build: quota of ComputeRequest must be a valid reference")
206
+ ll.assert(queue_name != "", "exec.limits.build: queue name is required for computeRequestBuilder")
207
+
208
+ requestBuilder := undefined
209
+
210
+ if useBatch(queue_name) {
211
+ requestBuilder = smart.ephemeralBuilder(_RTYPE_COMPUTE_REQUEST_BATCH_V1)
212
+ } else {
213
+ requestBuilder = smart.ephemeralBuilder(_RTYPE_COMPUTE_REQUEST_EXECUTOR_V1)
214
+ }
191
215
 
192
- rType := _RTYPE_COMPUTE_REQUEST(executor)
193
- requestBuilder := smart.ephemeralBuilder(rType)
194
216
  requestBuilder.getField(_CR_FIELD_REQUEST).set(quota)
195
217
 
218
+ requestRef := requestBuilder.lockAndBuild()
219
+
196
220
  return ll.toStrict(
197
- oop.inherit(requestBuilder, {
198
- allocation: requestBuilder.getField(_CR_FIELD_ALLOCATION)
221
+ oop.inherit(requestRef, {
222
+ allocation: requestRef.getField(_CR_FIELD_ALLOCATION)
199
223
  })
200
224
  )
201
225
  }
@@ -205,6 +229,7 @@ computeRequestBuilder := func() {
205
229
  }
206
230
 
207
231
  export ll.toStrict({
232
+ useBatch: useBatch,
208
233
  quotaBuilder: quotaBuilder,
209
234
  storageSpaceRequestBuilder: storageSpaceRequestBuilder,
210
235
  computeRequestBuilder: computeRequestBuilder
@@ -15,7 +15,8 @@ slices := import("@platforma-sdk/workflow-tengo:slices")
15
15
  limits := import("@platforma-sdk/workflow-tengo:exec.limits")
16
16
 
17
17
  _RTYPE_RUN_COMMAND_V1 := { Name: "RunCommand/executor", Version: "1" }
18
- _RTYPE_RUN_COMMAND_V2 := { Name: "RunCommand/executor", Version: "2" }
18
+ _RTYPE_RUN_COMMAND_EXEC_V2 := { Name: "RunCommand/executor", Version: "2" }
19
+ _RTYPE_RUN_COMMAND_BATCH_V2 := { Name: "RunCommand/batch", Version: "2" }
19
20
  _RTYPE_RUN_COMMAND_CMD := { Name: "RunCommandCmd", Version: "1" }
20
21
  _RTYPE_RUN_COMMAND_ARGS := { Name: "RunCommandArgs", Version: "1" }
21
22
  _RTYPE_RUN_COMMAND_OPTIONS := { Name: "run-command/options", Version: "1" }
@@ -77,16 +78,6 @@ _createExpressionArg := func(argTpl) {
77
78
 
78
79
 
79
80
  builder := func(workdir) {
80
- res := undefined
81
-
82
- if feats.computeLimits {
83
- res = smart.ephemeralBuilder(_RTYPE_RUN_COMMAND_V2)
84
- } else {
85
- res = smart.ephemeralBuilder(_RTYPE_RUN_COMMAND_V1)
86
- }
87
-
88
- res.getField(_FIELD_WORKDIR_IN).set(workdir)
89
-
90
81
  self := undefined
91
82
 
92
83
  cmd := undefined
@@ -105,7 +96,7 @@ builder := func(workdir) {
105
96
  stderr := "stderr.txt"
106
97
  nErrorLines := 200
107
98
 
108
- self = ll.toStrict(oop.inherit(res, {
99
+ self = ll.toStrict({
109
100
 
110
101
 
111
102
 
@@ -387,6 +378,25 @@ builder := func(workdir) {
387
378
 
388
379
 
389
380
  build: func() {
381
+ runRes := undefined
382
+
383
+ useBatch := limits.useBatch(queue)
384
+
385
+ if useBatch {
386
+
387
+
388
+
389
+ runRes = smart.ephemeralBuilder(_RTYPE_RUN_COMMAND_BATCH_V2)
390
+
391
+ } else if feats.computeLimits {
392
+ runRes = smart.ephemeralBuilder(_RTYPE_RUN_COMMAND_EXEC_V2)
393
+
394
+ } else {
395
+ runRes = smart.ephemeralBuilder(_RTYPE_RUN_COMMAND_V1)
396
+ }
397
+
398
+ runRes.getField(_FIELD_WORKDIR_IN).set(workdir)
399
+
390
400
 
391
401
 
392
402
  ll.assert(!hasPatternArgs || len(substRules) != 0,
@@ -396,21 +406,23 @@ builder := func(workdir) {
396
406
  "runcmd.builder().build: expression arguments must be enabled only if platforma backend supports expressions, found: %v",
397
407
  hasExpressionArgs)
398
408
 
399
- if feats.computeLimits {
409
+ if feats.computeLimits || useBatch {
400
410
  if !smart.isReference(_allocation) {
401
411
 
402
412
 
403
- _quota := limits.quotaBuilder().queue(queue).build()
404
- _allocation = limits.computeRequestBuilder().quota(_quota).build().allocation
413
+ _quotaRef := limits.quotaBuilder().queue(queue).build()
414
+ _resReqRef := limits.computeRequestBuilder().queue(queue).quota(_quotaRef).build()
415
+ _allocation = _resReqRef.allocation
405
416
  }
406
- self.getField(_FIELD_ALLOCATION).set(_allocation)
417
+
418
+ runRes.getField(_FIELD_ALLOCATION).set(_allocation)
407
419
  }
408
420
 
409
421
  refsRes := smart.createMapResourceWithType(_RTYPE_RUN_COMMAND_REFS, refs)
410
- self.getField(_FIELD_REFS).set(refsRes)
422
+ runRes.getField(_FIELD_REFS).set(refsRes)
411
423
 
412
424
  cmdRes := smart.createValueResource(_RTYPE_RUN_COMMAND_CMD, json.encode(cmd))
413
- self.getField(_FIELD_CMD).set(cmdRes)
425
+ runRes.getField(_FIELD_CMD).set(cmdRes)
414
426
 
415
427
  options := {
416
428
  queueName: queue,
@@ -423,19 +435,19 @@ builder := func(workdir) {
423
435
  }
424
436
 
425
437
  optionsRes := smart.createValueResource(_RTYPE_RUN_COMMAND_OPTIONS, json.encode(options))
426
- self.getField(_FIELD_OPTIONS).set(optionsRes)
438
+ runRes.getField(_FIELD_OPTIONS).set(optionsRes)
427
439
 
428
440
  argsRes := smart.createValueResource(_RTYPE_RUN_COMMAND_ARGS, json.encode(args))
429
- self.getField(_FIELD_ARGS).set(argsRes)
441
+ runRes.getField(_FIELD_ARGS).set(argsRes)
430
442
 
431
- res := ll.toStrict(oop.inherit(self.lockAndBuild(), {
432
- "workdir": self.outputs()[_FIELD_WORKDIR_OUT],
443
+ res := ll.toStrict(oop.inherit(runRes.lockAndBuild(), {
444
+ "workdir": runRes.outputs()[_FIELD_WORKDIR_OUT],
433
445
  "options": options
434
446
  }))
435
447
 
436
448
  return res
437
449
  }
438
- }))
450
+ })
439
451
 
440
452
  return self
441
453
  }
@@ -18,9 +18,25 @@ _isEnabled := func(featName) {
18
18
  return _checker(featName)
19
19
  }
20
20
 
21
+ _executors := undefined
22
+ _hasRunner := func(e_id) {
23
+ if is_undefined(_executors) {
24
+ _executors = {}
25
+ for e in plapi.getExecutors() {
26
+ _executors[e] = true
27
+ }
28
+ }
29
+
30
+ return !is_undefined(_executors[e_id])
31
+ }
32
+
21
33
  export ll.toStrict({
34
+ hasExec: _hasRunner("executor"), // local binary execution is available
35
+ hasBatch: _hasRunner("batch"), // batch system execution is available
36
+
22
37
  pureFutureFields: _isEnabled("pureFutureFields") || _api >= 1, // 'pure' future fields magic that provides deduplication
23
38
  commandExpressions: _isEnabled("commandExpressions") || _api >= 2, // rich expressions in command arguments ( {{ system.ram_bytes / 20 }} )
24
39
  workdirLimits: _isEnabled("workdirLimits") || _api >= 4, // workdir storage controller can limit concurrency
25
- computeLimits: _isEnabled("computeLimits") // runner can plan CPU and RAM
40
+ computeLimits: _isEnabled("computeLimits"), // runner can plan CPU and RAM
41
+ resourceDuplicateEvent: false // template can subscribe to ResourceDuplicate event
26
42
  })
@@ -685,6 +685,22 @@ resource = func(r) {
685
685
 
686
686
 
687
687
 
688
+ createDynamicField: func(name) {
689
+ if is_bytes(name) {
690
+ name = string(name)
691
+ }
692
+ ll.assert(is_string(name) && name != "", "field name must be a non-empty string")
693
+ fid := ll.fieldId(self.id, name)
694
+ tx.createField(fid, constants.FTYPE_DYNAMIC)
695
+ return field(fid)
696
+ },
697
+
698
+
699
+
700
+
701
+
702
+
703
+
688
704
 
689
705
  kvGet: func(key) {
690
706
  return tx.resourceKeyValueGet(self.id, key)
@@ -102,7 +102,8 @@ AWAIT_TEMPLATES := {
102
102
  "PColumnBundleWithPartitions": [
103
103
  [{ wildcard: "*" }, { match: "^ref$" }, "ResourceReady"], // optional ref field
104
104
  [{ wildcard: "*" }, "spec", "ResourceReady"],
105
- [{ wildcard: "*" }, "data", "InputsLocked"]
105
+ [{ wildcard: "*" }, "data", "InputsLocked"],
106
+ [{ wildcard: "*" }, "data", { match: "^\\{.*\\}$" }, "InputsLocked"] // for multi-hit BQuery results; key is always ref
106
107
  ]
107
108
  }
108
109
 
@@ -132,6 +133,9 @@ tpl := func(ops) {
132
133
  _stateRequests := []
133
134
  _delegatedOutputs := []
134
135
 
136
+
137
+
138
+
135
139
  self = {
136
140
 
137
141
 
@@ -565,7 +569,7 @@ tpl := func(ops) {
565
569
  if _hasPrepared {
566
570
  _stateRequests = append(_stateRequests, {
567
571
  path: [tplutil.PREPARE_MAP_FIELD_NAME],
568
- state: "ResourceReady"
572
+ state: "Final"
569
573
  })
570
574
  }
571
575
 
@@ -7,6 +7,9 @@ tx := import("tx")
7
7
  ll := import("@platforma-sdk/workflow-tengo:ll")
8
8
  text := import("text")
9
9
  regexp := import("@platforma-sdk/workflow-tengo:regexp")
10
+ smart := import("@platforma-sdk/workflow-tengo:smart")
11
+ maps := import("@platforma-sdk/workflow-tengo:maps")
12
+ feats := import("@platforma-sdk/workflow-tengo:feats")
10
13
 
11
14
  createContext := func() {
12
15
  return {
@@ -15,6 +18,9 @@ createContext := func() {
15
18
  }
16
19
  }
17
20
 
21
+
22
+ RTYPE_AWAITER := { Name: "awaiter", Version: "1" }
23
+
18
24
  awaitOneState := func(field, path, state) {
19
25
  if field.isErrorSet() && !field.isValueSet() {
20
26
 
@@ -51,9 +57,6 @@ awaitOneState := func(field, path, state) {
51
57
  subscriptions: [{
52
58
  resource: resource.id,
53
59
  event: "FieldCreated"
54
- },{
55
- resource: resource.id,
56
- event: "InputsLocked"
57
60
  }]
58
61
  }
59
62
  }
@@ -93,19 +96,19 @@ awaitOneState := func(field, path, state) {
93
96
  }
94
97
 
95
98
  subscriptions := []
96
- for nextFieldName, nextField in resource.inputs() {
99
+ maps.forEach(resource.inputs(), func(nextFieldName, nextField) {
97
100
  if !matcher(nextFieldName) {
98
- continue
101
+ return
99
102
  }
100
103
  res := awaitOneState(nextField, rPath, state)
101
104
  if res.type == "Ready" {
102
- continue
105
+ return
103
106
  } else if res.type == "Subscribe" {
104
107
  subscriptions = append(subscriptions, res.subscriptions...)
105
108
  } else {
106
109
  ll.panic("Unknown result type: %v", res.type)
107
110
  }
108
- }
111
+ })
109
112
  if len(subscriptions) == 0 {
110
113
  return { type: "Ready" }
111
114
  } else {
@@ -157,7 +160,7 @@ awaitOneState := func(field, path, state) {
157
160
  }]
158
161
  }
159
162
  }
160
- } else if state == "ResourceReady" {
163
+ } else if state == "ResourceReady" || state == "Final" {
161
164
 
162
165
 
163
166
  if resource.isFinal() {
@@ -167,7 +170,7 @@ awaitOneState := func(field, path, state) {
167
170
  type: "Subscribe",
168
171
  subscriptions: [{
169
172
  resource: resource.id,
170
- event: "ResourceReady"
173
+ event: "Final" // will also effectively subscribe to ResourceDuplicate and ResourceError
171
174
  }]
172
175
  }
173
176
  }
@@ -178,19 +181,21 @@ awaitOneState := func(field, path, state) {
178
181
  }
179
182
  }
180
183
 
184
+ _renderer := smart.resource(ll.getCurrentTemplateRenderer())
185
+
181
186
 
182
187
  await := func(rootField, ctxStorageResource, requests) {
183
188
  pssKey := "await:" + string(rootField.id.ResourceID) + ":" + rootField.id.Name
184
189
  prevSubscriptions := ctxStorageResource.kvGetAsJson(pssKey)
185
190
 
186
- if !is_undefined(prevSubscriptions) && len(prevSubscriptions) == 0 {
191
+ lastSubscriptionKey := undefined
192
+ if !is_undefined(prevSubscriptions) {
193
+ if is_undefined(prevSubscriptions.lastSubscriptionKey) {
187
194
 
188
- ll.panic("Unexpected state, desired state was reached in the previous call")
189
- }
190
-
191
- if is_undefined(prevSubscriptions) {
192
-
193
- prevSubscriptions = {}
195
+ ll.panic("Unexpected state, desired state was reached in the previous call")
196
+ } else {
197
+ lastSubscriptionKey = prevSubscriptions.lastSubscriptionKey
198
+ }
194
199
  }
195
200
 
196
201
  subscriptions := {}
@@ -222,34 +227,88 @@ await := func(rootField, ctxStorageResource, requests) {
222
227
  }
223
228
 
224
229
 
225
- for key, sub in subscriptions {
226
- if !is_undefined(prevSubscriptions[key]) {
227
230
 
228
- continue
231
+
232
+
233
+
234
+
235
+
236
+
237
+ firstSubscriptionKey := undefined
238
+ firstSubscription := undefined
239
+ lastSubscriptionKeyPresent := false
240
+ maps.forEach(subscriptions, func(key, sub) {
241
+ if is_undefined(firstSubscriptionKey) {
242
+ firstSubscriptionKey = key
243
+ firstSubscription = sub
229
244
  }
230
- e := {}
231
- e[sub.event] = true
232
- tx.subscribeTo(sub.resource, pssKey + ":" + key, e)
245
+ if !is_undefined(lastSubscriptionKey) && lastSubscriptionKey == key {
246
+ lastSubscriptionKeyPresent = true
247
+ }
248
+ })
249
+
250
+
251
+
252
+ if lastSubscriptionKeyPresent {
253
+ return false
233
254
  }
234
255
 
235
256
 
236
- for key, _ in prevSubscriptions {
237
- if !is_undefined(subscriptions[key]) {
257
+ if !is_undefined(firstSubscriptionKey) {
238
258
 
239
- continue
259
+ fullEventId := pssKey + ":" + firstSubscriptionKey
260
+ targetResource := firstSubscription.resource
261
+ targetEvents := [firstSubscription.event]
262
+
263
+ if firstSubscription.event == "Final" { // special case
264
+ if !feats.resourceDuplicateEvent {
265
+
266
+
267
+
268
+
269
+
270
+
271
+ target := smart.resource(targetResource)
272
+ isEphemeral := target.info().Features.ephemeral
273
+ if !isEphemeral { // doing proxy-object workaround for pure resources only
274
+ awaiterBuilder := smart.ephemeralBuilder(RTYPE_AWAITER)
275
+ awaiterBuilder.createInputField("target").set(target)
276
+ awaiter := awaiterBuilder.lockAndBuild()
277
+ _renderer.createDynamicField("tmp:" + fullEventId).set(awaiter)
278
+ targetResource = awaiter.info().ID // converting to global resource id; this is a hack to overcome the fact that tx.subscribeTo can't work with local ids
279
+ }
280
+ targetEvents = ["ResourceReady", "ResourceError"]
281
+
282
+ } else {
283
+ targetEvents = ["ResourceReady", "ResourceError", "ResourceDuplicate"]
284
+ }
285
+ } else if firstSubscription.event == "FieldCreated" { // another special case
286
+
287
+ targetEvents = ["FieldCreated", "InputsLocked"]
240
288
  }
241
- tx.unsubscribeFrom(pssKey + ":" + key)
289
+
290
+ e := {}
291
+ for event in targetEvents {
292
+ e[event] = true
293
+ }
294
+ tx.subscribeTo(targetResource, fullEventId, e)
295
+
296
+ }
297
+
298
+
299
+ if !is_undefined(lastSubscriptionKey) {
300
+ tx.unsubscribeFrom(pssKey + ":" + lastSubscriptionKey)
242
301
  }
243
302
 
244
303
 
245
304
  newPrevSubscriptions := {}
246
- for key, _ in subscriptions {
247
- newPrevSubscriptions[key] = true
305
+ if !is_undefined(firstSubscriptionKey) {
306
+ newPrevSubscriptions.lastSubscriptionKey = firstSubscriptionKey
248
307
  }
249
308
 
250
309
  ctxStorageResource.kvSetJson(pssKey, newPrevSubscriptions)
251
310
 
252
- return len(subscriptions) == 0
311
+ return is_undefined(firstSubscriptionKey)
253
312
  }
254
313
 
255
314
  export ll.toStrict({
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platforma-sdk/workflow-tengo",
3
- "version": "4.6.5",
3
+ "version": "4.7.1",
4
4
  "description": "Tengo SDK library",
5
5
  "types": "./dist/index.d.ts",
6
6
  "main": "./dist/index.js",