@live-change/simple-query 0.9.162 → 0.9.164

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@live-change/simple-query",
3
- "version": "0.9.162",
3
+ "version": "0.9.164",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -22,7 +22,7 @@
22
22
  },
23
23
  "type": "module",
24
24
  "dependencies": {
25
- "@live-change/framework": "^0.9.162",
25
+ "@live-change/framework": "^0.9.164",
26
26
  "pluralize": "^8.0.0"
27
27
  },
28
28
  "devDependencies": {
@@ -30,5 +30,5 @@
30
30
  "typedoc-plugin-markdown": "^4.6.3",
31
31
  "typedoc-plugin-rename-defaults": "^0.7.3"
32
32
  },
33
- "gitHead": "59cd1485ca6d76bd87a6634a92d6e661e5803d5e"
33
+ "gitHead": "38f9fb8b01a9527d8f6036e174edd1fa41443301"
34
34
  }
package/src/query.ts CHANGED
@@ -2,7 +2,16 @@ import {
2
2
  PropertyDefinitionSpecification, ServiceDefinition, ServiceDefinitionSpecification
3
3
  } from "@live-change/framework"
4
4
 
5
- import { ModelDefinition, ForeignModelDefinition } from "@live-change/framework"
5
+ import {
6
+ ModelDefinition, ForeignModelDefinition, ContextBase, QueryParameters as FrameworkQueryParameters,
7
+ QueryDefinition as FrameworkQueryDefinition,
8
+ QueryDefinitionSpecification as FrameworkQueryDefinitionSpecification,
9
+ ViewDefinition,
10
+ EventDefinition,
11
+ TriggerDefinition,
12
+ ActionDefinition,
13
+ ViewDefinitionSpecificationDaoPath
14
+ } from "@live-change/framework"
6
15
 
7
16
  import { PropertyDefinition } from "@live-change/framework"
8
17
 
@@ -17,7 +26,7 @@ const simpleQueryCode = readFileSync(simpleQueryCodePath, 'utf8')
17
26
 
18
27
 
19
28
  interface Range {
20
- gt?: string
29
+ gt?: string
21
30
  gte?: string
22
31
  lt?: string
23
32
  lte?: string,
@@ -51,7 +60,6 @@ class RuleSource {
51
60
  dependsOn: RuleSource[]
52
61
  index: IndexInfo | null
53
62
 
54
-
55
63
  constructor(rule: QueryRule, input: QueryInput, source: QuerySource, type: string) {
56
64
  this.rule = rule
57
65
  this.input = input
@@ -94,6 +102,14 @@ interface QueryDefinitionSpecification {
94
102
  sources?: Record<string, QuerySource>,
95
103
  code?: QueryCode,
96
104
  update?: boolean,
105
+ id: Function,
106
+ timeout?: number,
107
+ requestTimeout?: number,
108
+ validation?: (parameters: FrameworkQueryParameters, context: ContextBase) => Promise<any>
109
+ view?: Record<string, any>
110
+ event?: Record<string, any>
111
+ trigger?: Record<string, any>
112
+ action?: Record<string, any>
97
113
  }
98
114
 
99
115
  class OutputMapping {
@@ -181,7 +197,8 @@ class IndexInfo {
181
197
  $_createQuery(service: ServiceDefinition<any>) {
182
198
  return new QueryDefinition(service, {
183
199
  name: this.name,
184
- properties: {}
200
+ properties: {},
201
+ id: (x:any) => x
185
202
  }, this.rules)
186
203
  }
187
204
  }
@@ -199,6 +216,7 @@ export class QueryDefinition<SDS extends ServiceDefinitionSpecification> {
199
216
  definition: QueryDefinitionSpecification
200
217
  properties: Record<string, PropertyDefinition<any>>
201
218
  rules: QueryRules
219
+ idFields: QueryInputLike[] | null
202
220
  firstRule: QueryRule
203
221
  rootSources: RuleSource[]
204
222
  ruleSources: RuleSource[]
@@ -207,6 +225,8 @@ export class QueryDefinition<SDS extends ServiceDefinitionSpecification> {
207
225
  executionPlan: ExecutionStep[]
208
226
  indexPlan: ExecutionStep[]
209
227
 
228
+ preparedQuery: FrameworkQueryDefinition<FrameworkQueryDefinitionSpecification>
229
+
210
230
  constructor(
211
231
  serviceDefinition: ServiceDefinition<SDS>, definition: QueryDefinitionSpecification, rules: QueryRule[] = undefined
212
232
  ) {
@@ -246,6 +266,15 @@ export class QueryDefinition<SDS extends ServiceDefinitionSpecification> {
246
266
  }
247
267
  }
248
268
 
269
+ printIdFields() {
270
+ console.log("ID FIELDS:")
271
+ if(this.idFields) {
272
+ for(const idField of this.idFields) {
273
+ console.log(` `, queryDescription(idField, ' '))
274
+ }
275
+ }
276
+ }
277
+
249
278
  computeRules() {
250
279
  const queryProperties = {}
251
280
  for(const propertyName in this.definition.properties) {
@@ -263,6 +292,7 @@ export class QueryDefinition<SDS extends ServiceDefinitionSpecification> {
263
292
 
264
293
  // run the code to collect relations
265
294
  this.rules = this.definition.code(queryProperties, queryInputs)
295
+ this.idFields = this.definition.id ? this.definition.id(queryInputs) : null
266
296
  }
267
297
 
268
298
  markStaticRules() {
@@ -340,7 +370,7 @@ export class QueryDefinition<SDS extends ServiceDefinitionSpecification> {
340
370
  for(const key in this.rules) {
341
371
  const rule = this.rules[key]
342
372
  console.log(`${indent} RULE ${key}:`)
343
- console.log(`${indent} ${queryDescription(rule, indent + ' ')}`)
373
+ console.log(`${indent} ${queryDescription(rule, indent + ' ')}`)
344
374
  console.log(`${indent} SOURCES:`)
345
375
  for(const ruleSource of this.ruleSources) {
346
376
  if(ruleSource.rule !== rule) continue
@@ -377,46 +407,52 @@ export class QueryDefinition<SDS extends ServiceDefinitionSpecification> {
377
407
  }
378
408
  }
379
409
 
380
- computeSourceExecutionPlan(source: RuleSource, resultParameters: string[]) {
381
- /// TODO: W poniższej linii jest bug, powinno pobierać odmienne source, a nie te bezpośrednio zależne.
382
- /// Poza tym przy pobieraniu wstecz nie koniecznie potrzeba indexów,
383
- /// może trzeba wprowadzić index-forward i index-backward ?
384
- /// A może trzeba wprowadzić analizę tego co mamy i co chcemy uzyskać w przyszłości ?
410
+ computeSourceExecutionPlan(source: RuleSource, resultParameters: string[]) {
411
+ /// TODO: Może trzeba wprowadzić analizę tego co mamy i co chcemy uzyskać w przyszłości ?
385
412
 
386
413
  const next = source.dependentBy.map(dependent => {
387
414
  const otherSource = this.ruleSources.find(s => s.rule === dependent.rule && s != dependent)
388
- return this.computeSourceExecutionPlan(otherSource, [...resultParameters, source.input.$alias])
415
+ const nextStep = this.computeSourceExecutionPlan(otherSource, [...resultParameters, source.input.$alias])
416
+ return nextStep
389
417
  })
390
418
 
391
419
  console.log("COMPUTING SOURCE EXECUTION PLAN FOR", source.input.$_toQueryDescription(), "WITH", resultParameters)
392
420
  console.log("RULE", queryDescription(source.rule, ' '))
393
421
 
394
422
  const ruleParameters = JSON.parse(JSON.stringify(source.rule.$_parametersJSON(resultParameters)))
423
+ console.log("RULE PARAMETERS", ruleParameters)
424
+ const mandatory = this.idFields?.find(idField => source.input.$alias === idField.$alias) ? true : undefined
395
425
  if(source.index) {
396
426
  const indexExecution = {
397
427
  ...source.index.$_executionJSON(),
398
- by: ruleParameters[Object.keys(ruleParameters)[0]]
428
+ by: parameterEnsureRange(ruleParameters[Object.keys(ruleParameters)[0]])
399
429
  }
430
+ /// TODO: decide if id field is mandatory - if exports aliases that are required in id Fields or market mandatory
400
431
  const indexNext = [{
401
- operation: 'object',
402
- ...source.input.$_executionJSON(),
403
- by: {
404
- type: 'result',
405
- path: [indexExecution.alias, source.index.indexParts.at(-1).alias],
432
+ execution: {
433
+ operation: 'object',
434
+ ...source.input.$_executionJSON(),
435
+ by: {
436
+ type: 'result',
437
+ path: [indexExecution.alias, source.index.indexParts.at(-1).alias],
438
+ },
406
439
  },
440
+ mandatory,
407
441
  next
408
442
  }]
409
443
  return {
410
444
  execution: indexExecution,
411
445
  next: indexNext
412
446
  }
413
- }
447
+ }
448
+
414
449
  const execution = {
415
- ...source.input.$_executionJSON(),
450
+ ...source.input.$_executionJSON(),
416
451
  by: ruleParameters[Object.keys(ruleParameters)[0]]
417
452
  }
418
453
  const executionPlan = {
419
454
  execution,
455
+ mandatory,
420
456
  next
421
457
  }
422
458
  return executionPlan
@@ -602,23 +638,46 @@ export class QueryDefinition<SDS extends ServiceDefinitionSpecification> {
602
638
  prepareQuery() {
603
639
  this.createIndexes()
604
640
 
605
- console.log("########### PREPARING QUERY!!!!")
641
+ console.log("########### PREPARING QUERY!!!!")
606
642
 
607
643
  this.printRules()
608
644
 
645
+ this.printIdFields()
646
+
609
647
  this.printDependencies()
610
648
 
611
649
  this.computeExecutionPlan()
612
650
  console.log("EXECUTION PLAN:")
613
651
  console.log(JSON.stringify(this.executionPlan, null, 2))
614
652
 
615
- process.exit(0)
616
- /// TODO: create indexes used by query
617
-
618
-
619
- /// TODO: prepare query
653
+ //process.exit(0)
620
654
 
621
- // process.exit(0)
655
+ const idFunctionCode = (typeof this.definition.id === 'string')
656
+ ? this.definition.id : `(${this.definition.id})`
657
+
658
+ console.log("ID FUNCTION", idFunctionCode)
659
+
660
+ this.preparedQuery = this.service.query({
661
+ name: this.definition.name,
662
+ properties: this.definition.properties,
663
+ returns: this.definition.returns,
664
+ code: simpleQueryCode,
665
+ sourceName: simpleQueryCodePath,
666
+ update: this.definition.update,
667
+ timeout: this.definition.timeout,
668
+ requestTimeout: this.definition.requestTimeout,
669
+ validation: this.definition.validation,
670
+ config: {
671
+ plan: this.executionPlan,
672
+ idFunction: idFunctionCode
673
+ },
674
+ view: this.definition.view,
675
+ trigger: this.definition.trigger,
676
+ action: this.definition.action,
677
+ event: this.definition.event
678
+ })
679
+
680
+ return this.preparedQuery
622
681
  }
623
682
 
624
683
  createIndex(name, mapping: OutputMapping[]) {
@@ -649,6 +708,22 @@ export class QueryDefinition<SDS extends ServiceDefinitionSpecification> {
649
708
  }
650
709
  }
651
710
 
711
+ function parameterIsRange(parameter: any) {
712
+ if(typeof parameter !== 'object' || parameter === null) return false
713
+ if(Array.isArray(parameter)) return parameterIsRange(parameter.at(-1))
714
+ if(parameter.type === 'object') return true
715
+ return false
716
+ }
717
+
718
+ function parameterEnsureRange(parameter: any) {
719
+ if(parameterIsRange(parameter)) return parameter
720
+ if(Array.isArray(parameter)) return [...parameter, { type: 'object', properties: {} }]
721
+ return {
722
+ type: 'array',
723
+ items: [parameter, { type: 'object', properties: {} }]
724
+ }
725
+ }
726
+
652
727
 
653
728
  export type QueryFactoryFunction<SDS extends ServiceDefinitionSpecification> =
654
729
  (definition: QueryDefinitionSpecification) => QueryDefinition<SDS>
@@ -675,7 +750,9 @@ function getSource(input: RuleInput): QuerySource {
675
750
  function isStatic(element: any) {
676
751
  if(typeof element !== "object" || element === null) return true
677
752
  return element instanceof CanBeStatic ? element.$_isStatic() :
678
- (element.constructor.name === "Object" ? element[staticRuleSymbol] : false)
753
+ ((element.constructor.name === "Object" || element.constructor.name === "Array")
754
+ ? element[staticRuleSymbol]
755
+ : false)
679
756
  }
680
757
 
681
758
  function queryDescription(element: any, indent: string = "") {
@@ -706,6 +783,7 @@ function markStatic(element: any) {
706
783
  function parameterJSON(element: any) {
707
784
  if(typeof element !== "object" || element === null) return element
708
785
  if(element instanceof QueryPropertyBase) return { type: 'property', path: element.$path }
786
+ if(Array.isArray(element)) return element.map(item => parameterJSON(item))
709
787
  const output = {
710
788
  type: 'object',
711
789
  properties: {}
@@ -768,7 +846,7 @@ export class RangeRule extends QueryRule {
768
846
  $_getSources(): RuleSource[] {
769
847
  return [
770
848
  new RuleSource(this, this.$input, getSource(this.$input), 'range')
771
- ].filter(s => s.input != null)
849
+ ].filter(s => s.input?.$source != null)
772
850
  }
773
851
 
774
852
  $_parametersJSON(resultParameters: string[]) {
@@ -814,7 +892,7 @@ export class EqualsRule extends QueryRule {
814
892
  return [
815
893
  new RuleSource(this, this.$inputA, getSource(this.$inputA), 'object'),
816
894
  new RuleSource(this, this.$inputB, getSource(this.$inputB), 'object')
817
- ].filter(s => s.input != null)
895
+ ].filter(s => s.input?.$source != null)
818
896
  }
819
897
 
820
898
  $_parametersJSON(resultParameters: string[]) {
@@ -18,7 +18,7 @@ async function autoIndex(input, output, { plan, properties }) {
18
18
 
19
19
  async function gatherOutputData(next, context, oldContext) {
20
20
  const outputContext = { ...context }
21
- const oldOutputContext = { ...context }
21
+ const oldOutputContext = { ...oldContext }
22
22
  /// first execute next to gather all data
23
23
  for(const nextStep of next) {
24
24
  await fetch(nextStep, outputContext, oldOutputContext)
@@ -1,11 +1,38 @@
1
1
  async function simpleQuery(input, output, { _query, ...params }) {
2
2
 
3
3
  const plan = _query.plan
4
- const idFunction = idFunction && eval(`(${_query.idFunction})`)
4
+ const idFunction = _query.idFunction && eval(`(${_query.idFunction})`)
5
5
 
6
6
  function sourceChangeStream(source, by) {
7
+ /// TODO: support arrays that have object - prefixed range queries
7
8
  if(typeof by === 'string') return source.object(by)
8
- if(Array.isArray(by)) return source.object(serializeKey(by))
9
+ if(Array.isArray(by)) {
10
+ if(typeof by.at(-1) === 'string') {
11
+ return source.object(serializeKey(by))
12
+ } else {
13
+ if(by.slice(0, -1).every(item => typeof item === 'string')) {
14
+ const prefix = serializeKeyData(by.slice(0, -1))
15
+ const range = by.at(-1)
16
+ const prefixedRange = {
17
+ gt: range.gt ? prefix + range.gt : undefined,
18
+ gte: range.gte ? prefix + range.gte : undefined,
19
+ lt: range.lt ? prefix + range.lt : undefined,
20
+ lte: range.lte ? prefix + range.lte : undefined,
21
+ reverse: range.reverse,
22
+ limit: range.limit,
23
+ }
24
+ if(!(prefixedRange.gt || prefixedRange.gte)) {
25
+ prefixedRange.gte = prefix
26
+ }
27
+ if(!(prefixedRange.lt || prefixedRange.lte)) {
28
+ prefixedRange.lte = prefix + "\xFF\xFF\xFF\xFF"
29
+ }
30
+ return source.range(prefixedRange)
31
+ } else {
32
+ throw new Error("Impossible to compute range from array: " + JSON.stringify(by))
33
+ }
34
+ }
35
+ }
9
36
  return source.range(by)
10
37
  }
11
38
 
@@ -37,6 +64,28 @@ async function simpleQuery(input, output, { _query, ...params }) {
37
64
  }
38
65
  }
39
66
 
67
+ function applyChange(results, obj, oldObj) {
68
+ if(oldObj) {
69
+ const index = results.findIndex(result => result.id === oldObj.id)
70
+ if(index !== -1) {
71
+ if(obj) {
72
+ results[index] = obj
73
+ } else {
74
+ results.splice(index, 1)
75
+ }
76
+ }
77
+ } else if(obj) {
78
+ const index = results.findIndex(result => result.id >= obj.id)
79
+ if(index === -1) {
80
+ results.push(obj)
81
+ } else if(results[index].id === obj.id) {
82
+ results[index] = obj
83
+ } else {
84
+ results.splice(index, 0, obj)
85
+ }
86
+ }
87
+ }
88
+
40
89
  class DataObservation {
41
90
 
42
91
  #planStep = null
@@ -44,20 +93,30 @@ async function simpleQuery(input, output, { _query, ...params }) {
44
93
  #source = null
45
94
  #by = null
46
95
  #onChange = null
47
-
96
+ #disposed = false
97
+
48
98
  #observation = null
49
99
 
50
- #dependentObservations = new Map()
100
+ #dependentObservationsByNext
51
101
 
52
102
  #resultsPromise = null
53
- #results = null
103
+ #results = []
104
+ #rawResults = []
105
+
106
+ #idFunction = null
54
107
 
55
- constructor(planStep, context, source, by, onChange) {
108
+ constructor(planStep, context, source, by, onChange, idFunction) {
56
109
  this.#planStep = planStep
57
110
  this.#context = context
58
111
  this.#source = source
59
112
  this.#by = by
60
113
  this.#onChange = onChange
114
+ this.#idFunction = idFunction
115
+
116
+ this.#dependentObservationsByNext = new Array(planStep.next.length)
117
+ for(const nextIndex in planStep.next) {
118
+ this.#dependentObservationsByNext[nextIndex] = new Map()
119
+ }
61
120
  }
62
121
 
63
122
  async start() {
@@ -65,111 +124,203 @@ async function simpleQuery(input, output, { _query, ...params }) {
65
124
  const context = this.#context
66
125
  if(!this.#source) this.#source = await getSource(planStep.execution.sourceType, planStep.execution.name)
67
126
  if(!this.#by) this.#by = decodeParameter(planStep.execution.by, context, params)
68
- let results = []
69
- const observationPromise = sourceChangeStream(this.#source, this.#by).onChange(async (obj, oldObj) => {
127
+ //output.debug("STARTING OBSERVATION", planStep.execution.sourceType, planStep.execution.name, "BY", this.#by)
128
+ const sourceStream = sourceChangeStream(this.#source, this.#by)
129
+ const observationPromise = sourceStream.onChange(async (obj, oldObj) => {
130
+ /* output.debug(this.#planStep.execution.alias, "!", planStep.execution.sourceType, planStep.execution.name, "BY", this.#by,
131
+ 'CHANGE', obj, oldObj) */
70
132
  const id = obj?.id || oldObj?.id
71
133
  if(!id) return
72
134
 
73
- if(!this.#results) { // still fetching
74
- results.push(obj)
75
- } else { // already fetched - maintain memory list of results
76
- if(oldObj) {
77
- const index = this.#results.findIndex(result => result.id === oldObj.id)
78
- if(index !== -1) {
79
- if(obj) {
80
- this.#results[index] = obj
81
- } else {
82
- this.#results.splice(index, 1)
83
- }
84
- }
85
- } else if(obj) {
86
- const index = this.#results.findIndex(result => result.id >= oldObj.id) // idempotency check
87
- if(index === -1) {
88
- results.push(obj)
89
- } else if(this.#results[index].id === oldObj.id) {
90
- this.#results[index] = obj
91
- } else {
92
- this.#results.splice(index, 0, obj)
93
- }
94
- }
95
- }
135
+ applyChange(this.#rawResults, obj, oldObj)
96
136
 
97
137
  const nextContext = { ...context, [planStep.execution.alias]: obj }
98
138
  const nextOldContext = { ...context, [planStep.execution.alias]: oldObj }
99
139
 
100
- const objectObservations = []
140
+ //const objectObservations = []
141
+
142
+ const oldJoinedResults = oldObj ? await this.#joinResults([ oldObj ]) : []
101
143
 
102
- for(const nextStep of planStep.next) {
144
+ for(const nextStepIndex in planStep.next) {
145
+ const nextStep = planStep.next[nextStepIndex]
103
146
  const nextSource = await getSource(nextStep.execution.sourceType, nextStep.execution.name)
104
- const nextBy = decodeParameter(nextStep.execution.by, nextContext, params)
105
- const nextOldBy = decodeParameter(nextStep.execution.by, nextOldContext, params)
106
- const nextByKey = nextBy && serializeKey([nextStep.execution.alias, nextBy])
107
- const nextOldByKey = nextOldBy && serializeKey([nextStep.execution.alias, nextOldBy])
147
+ const nextBy = obj && decodeParameter(nextStep.execution.by, nextContext, params)
148
+ const nextOldBy = oldObj && decodeParameter(nextStep.execution.by, nextOldContext, params)
149
+ const nextByKey = nextBy && serializeKeyData(nextBy)
150
+ const nextOldByKey = nextOldBy && serializeKeyData(nextOldBy)
108
151
  if(nextByKey !== nextOldByKey) {
109
- if(this.#dependentObservations.has(nextOldByKey)) {
110
- const dependentObservation = this.#dependentObservations.get(nextOldByKey)
152
+ const nextDependentObservations = this.#dependentObservationsByNext[nextStepIndex]
153
+ if(nextOldBy && nextDependentObservations.has(nextOldByKey)) {
154
+ const dependentObservation = nextDependentObservations.get(nextOldByKey)
111
155
  dependentObservation.dispose()
112
- this.#dependentObservations.delete(nextOldByKey)
156
+ nextDependentObservations.delete(nextOldByKey)
113
157
  }
114
- if(!this.#dependentObservations.has(nextByKey)) {
158
+ if(nextBy && !nextDependentObservations.has(nextByKey)) {
115
159
  const dependentObservation = new DataObservation(nextStep, nextContext, nextSource, nextBy,
116
160
  (context, oldContext, observation) => this.handleDependentChange(context, oldContext, observation, id))
117
- this.#dependentObservations.set(nextByKey, dependentObservation)
161
+ nextDependentObservations.set(nextByKey, dependentObservation)
118
162
  await dependentObservation.start()
119
163
  }
120
164
  }
121
- if(nextByKey) objectObservations.push(this.#dependentObservations.get(nextByKey))
122
- }
165
+ //if(nextByKey) objectObservations.push(this.#dependentObservations.get(nextByKey))
166
+ }
167
+
168
+ const newJoinedResults = obj ? await this.#joinResults([ obj ]) : []
169
+
170
+ //output.debug(this.#planStep.execution.alias, "oldJoinedResults", oldJoinedResults, "from", oldObj)
171
+ //output.debug(this.#planStep.execution.alias, "newJoinedResults", newJoinedResults, "from", obj)
172
+
173
+ await this.#joinedChanges(newJoinedResults, oldJoinedResults)
123
174
 
124
- /// TODO: do object observations cross product, add self, and push change to parent
125
175
  })
126
176
  this.#resultsPromise = observationPromise.then(() => {
127
- this.#results = results
128
- return results
177
+ return this.#results
129
178
  })
130
179
  this.#observation = await observationPromise
131
180
  }
132
181
 
182
+ async #joinedChange(obj, oldObj) {
183
+ applyChange(this.#results, obj, oldObj)
184
+ await this.#onChange(obj, oldObj, this)
185
+ }
186
+
187
+ async #joinedChanges(newJoinedResults, oldJoinedResults) {
188
+ for(const oldJoinedResult of oldJoinedResults) {
189
+ if(!oldJoinedResult) throw new Error("oldJoinedResult is null")
190
+ if(!newJoinedResults.some(newResult => newResult.id === oldJoinedResult.id))
191
+ this.#joinedChange(null, oldJoinedResult)
192
+ }
193
+ for(const newJoinedResult of newJoinedResults) {
194
+ const oldJoinedResult = oldJoinedResults.find(oldResult => oldResult.id === newJoinedResult.id)
195
+ if(oldJoinedResult) {
196
+ if(JSON.stringify(newJoinedResult) !== JSON.stringify(oldJoinedResult)) {
197
+ this.#joinedChange(newJoinedResult, oldJoinedResult)
198
+ }
199
+ } else {
200
+ this.#joinedChange(newJoinedResult, null)
201
+ }
202
+ }
203
+ }
204
+
205
+ async #joinResults(results) {
206
+ let joinedResults = results.map(result => ({
207
+ __result_id_patrs: [result.id],
208
+ [this.#planStep.execution.alias]: result
209
+ }))
210
+ //output.debug("joined results", joinedResults)
211
+ for(const nextId in this.#planStep.next) {
212
+ const nextDependentObservations = this.#dependentObservationsByNext[nextId]
213
+ const nextStep = this.#planStep.next[nextId]
214
+ // output.debug(" dep results", dependentObservation.#planStep.execution.alias, dependentResults)
215
+ joinedResults = (await Promise.all(joinedResults.map(
216
+ async (joinedResult) => {
217
+ const nextBy = decodeParameter(nextStep.execution.by, joinedResult, params)
218
+ const nextByKey = nextBy && serializeKeyData(nextBy)
219
+ const dependentObservation = nextDependentObservations.get(nextByKey)
220
+ let dependentResults = []
221
+ if(dependentObservation) {
222
+ dependentResults = await dependentObservation.results()
223
+ }
224
+ if(dependentResults.length === 0) {
225
+ if(nextStep.mandatory) return []
226
+ return [
227
+ { /// TODO: check if optional (not mandatory)
228
+ ...joinedResult,
229
+ [nextStep.execution.alias]: null
230
+ }
231
+ ]
232
+ }
233
+ return dependentResults.map(dependentJoinedResult => ({
234
+ ...dependentJoinedResult,
235
+ ...joinedResult,
236
+ __result_id_patrs: joinedResult.__result_id_patrs.concat(dependentJoinedResult.__result_id_patrs)
237
+ }))
238
+ }
239
+ ))).flat()
240
+ }
241
+ //output.debug("joinedResultsAfterDependencies", JSON.stringify(joinedResults, null, 2))
242
+ for(const joinedResult of joinedResults) {
243
+ /* if(this.#idFunction) {
244
+ output.debug("callIdFunction", _query.idFunction, "on", JSON.stringify(joinedResult, null, 2))
245
+ } */
246
+ joinedResult.id = serializeKey(
247
+ this.#idFunction ? this.#idFunction(joinedResult) : joinedResult.__result_id_patrs
248
+ )
249
+ }
250
+ return joinedResults
251
+ }
252
+
133
253
  async results() {
134
254
  if(!this.#results) return this.#results
135
255
  return await this.#resultsPromise
136
256
  }
137
257
 
138
258
  async handleDependentChange(context, oldContext, observation, id) {
139
- const currentResult = this.#results.find(result => result.id === id)
140
- if(!currentResult) return /// or delete associated objects
141
- const currentContext = { [this.#planStep.execution.alias]: currentResult }
142
- await this.#onChange({
143
- ...currentContext,
144
- ...context,
145
- ...currentContext,
146
- }, {
147
- ...currentContext,
148
- ...oldContext,
149
- ...currentContext,
150
- }, this)
259
+ if(!this.#results) return
260
+ if(observation.#disposed) return
261
+
262
+ /* console.log("handleDependentChange", context, oldContext, id,
263
+ "observation", observation.#planStep.execution.alias,
264
+ "to", this.#planStep.execution.alias) */
265
+
266
+ const observationByJson = JSON.stringify(observation.#by)
267
+
268
+ const affectedRawResults = this.#rawResults.filter(result =>
269
+ JSON.stringify(decodeParameter(observation.#planStep.execution.by, {
270
+ [this.#planStep.execution.alias]: result
271
+ }, params)) === observationByJson
272
+ )
273
+
274
+ //console.log("affectedRawResults", affectedRawResults)
275
+
276
+ for(const affectedRawResult of affectedRawResults) {
277
+ const id = affectedRawResult.id
278
+ const newJoinedResults = await this.#joinResults([ affectedRawResult ])
279
+ const oldJoinedResults = this.#results.filter(
280
+ joinedResult => joinedResult[this.#planStep.execution.alias].id === id
281
+ )
282
+ //console.log("newJoinedResults", newJoinedResults)
283
+ //console.log("oldJoinedResults", oldJoinedResults)
284
+ await this.#joinedChanges(newJoinedResults, oldJoinedResults)
285
+ }
151
286
  }
152
287
 
153
288
  dispose() {
154
289
  this.#observation.dispose()
155
- this.#dependentObservations.forEach(dependentObservation => dependentObservation.dispose())
156
- this.#dependentObservations.clear()
157
- this.#results = null
290
+ for(const nextIndex in this.#dependentObservationsByNext) {
291
+ const nextDependentObservations = this.#dependentObservationsByNext[nextIndex]
292
+ for(const dependentObservation of nextDependentObservations.values()) {
293
+ dependentObservation.dispose()
294
+ }
295
+ nextDependentObservations.clear()
296
+ }
297
+ this.#results = []
298
+ this.#rawResults = []
158
299
  this.#resultsPromise = null
159
300
  this.#observation = null
160
301
  this.#source = null
161
302
  this.#by = null
162
303
  this.#planStep = null
163
304
  this.#context = null
305
+ this.#disposed = true
164
306
  }
165
307
  }
166
308
 
167
- const rootObservations = plan.map(step => new DataObservation(step, {}, null, null, async (newContext, oldContext, observation) => {
168
- const idParts = idFunction ? idFunction(newContext) : Object.values(newContext).map(v => v?.id)
169
- const id = serializeKey(idParts)
170
- await output.change({ ...newContext, id }, { ...oldContext, id })
171
- }))
309
+ const rootObservations = plan.map(step => new DataObservation(step, {}, null, null,
310
+ (result, oldResult, observation) => {
311
+ if(result) delete result.__result_id_patrs
312
+ if(oldResult) delete oldResult.__result_id_patrs
313
+ output.change(result, oldResult)
314
+ },
315
+ idFunction
316
+ /* async (newContext, oldContext, observation) => {
317
+ const idParts = idFunction ? idFunction(newContext) : Object.values(newContext).map(v => v?.id)
318
+ const id = serializeKey(idParts)
319
+ await output.change({ ...newContext, id }, { ...oldContext, id })
320
+ } */
321
+ ))
172
322
 
173
323
  await Promise.all(rootObservations.map(observation => observation.start()))
324
+ await Promise.all(rootObservations.map(observation => observation.results()))
174
325
 
175
326
  }