@inseefr/lunatic 3.6.11 → 3.6.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.
@@ -41,15 +41,30 @@ export type EventArgs = {
41
41
  [extra: string]: unknown;
42
42
  };
43
43
  };
44
+
44
45
  export type LunaticVariablesStoreEvent<T extends keyof EventArgs> = {
45
46
  detail: EventArgs[T];
46
47
  };
47
48
 
49
+ /**
50
+ * Represent a point in time (more precise than Date)
51
+ */
52
+ class Timekey {
53
+ private time = performance.now();
54
+ getTime() {
55
+ return this.time;
56
+ }
57
+ touch() {
58
+ this.time = performance.now();
59
+ }
60
+ }
61
+
48
62
  export class LunaticVariablesStore {
49
63
  private dictionary = new Map<string, LunaticVariable>();
50
64
  private eventTarget = new EventTarget();
51
65
  private queue = new Map<string, () => void>();
52
66
  public autoCommit = false; // Commit change instantly (used in tests)
67
+ public updatedAt = new Timekey();
53
68
 
54
69
  constructor() {
55
70
  interpretCount = 0;
@@ -65,6 +80,7 @@ export class LunaticVariablesStore {
65
80
  autoCommit?: boolean
66
81
  ) {
67
82
  const store = new LunaticVariablesStore();
83
+ (window as any).lunaticStore = store; // Allow access to the store from the console
68
84
  if (!source.variables) {
69
85
  return store;
70
86
  }
@@ -107,6 +123,7 @@ export class LunaticVariablesStore {
107
123
  }
108
124
  resizingBehaviour(store, source.resizing);
109
125
  missingBehaviour(store, source.missingBlock);
126
+ store.updatedAt.touch();
110
127
  return store;
111
128
  }
112
129
 
@@ -176,20 +193,14 @@ export class LunaticVariablesStore {
176
193
  'iteration' | 'cause' | 'ignoreIterationOnScalar'
177
194
  > = {}
178
195
  ): LunaticVariable {
196
+ this.updatedAt.touch();
179
197
  if (!this.dictionary.has(name)) {
180
198
  this.dictionary.set(
181
199
  name,
182
200
  new LunaticVariable({
183
201
  name,
184
- })
185
- );
186
- this.eventTarget.dispatchEvent(
187
- new CustomEvent('change', {
188
- detail: {
189
- ...args,
190
- name: name,
191
- value: value,
192
- } satisfies EventArgs['change'],
202
+ dependencies: [],
203
+ storeUpdatedAt: this.updatedAt,
193
204
  })
194
205
  );
195
206
  }
@@ -218,10 +229,12 @@ export class LunaticVariablesStore {
218
229
  dependencies,
219
230
  iterationDepth,
220
231
  shapeFrom,
232
+ dimension,
221
233
  }: {
222
234
  dependencies?: string[];
223
235
  iterationDepth?: number;
224
236
  shapeFrom?: string | string[];
237
+ dimension?: number;
225
238
  } = {}
226
239
  ): LunaticVariable {
227
240
  if (this.dictionary.has(name)) {
@@ -234,8 +247,11 @@ export class LunaticVariablesStore {
234
247
  dependencies,
235
248
  iterationDepth,
236
249
  name,
250
+ dimension,
251
+ storeUpdatedAt: this.updatedAt,
237
252
  });
238
253
  this.dictionary.set(name, variable);
254
+ this.updatedAt.touch();
239
255
  return variable;
240
256
  }
241
257
 
@@ -302,12 +318,16 @@ export class LunaticVariablesStore {
302
318
  class LunaticVariable {
303
319
  /** Last time the value was updated (changed). */
304
320
  public updatedAt = new Map<undefined | string, number>();
321
+ /** Last time the store was updated (changed). */
322
+ private storeUpdatedAt: Timekey;
305
323
  /** Last time "calculation" was run (for calculated variable). */
306
324
  private calculatedAt = new Map<undefined | string, number>();
307
325
  /** Internal value for the variable. */
308
326
  private value: unknown;
309
- /** List of dependencies, ex: ['FIRSTNAME', 'LASTNAME']. */
327
+ /** List of direct dependencies, ex: ['FULLNAME', 'FIRSTNAME', 'LASTNAME']. */
310
328
  private dependencies?: string[];
329
+ /** List of deep dependencies, exploring the variables used in calculated variables, ex: ['FIRSTNAME', 'LASTNAME']. */
330
+ private baseDependencies?: string[];
311
331
  /** Expression for calculated variable. */
312
332
  public readonly expression?: string;
313
333
  /** Dictionary holding all the available variables. */
@@ -320,17 +340,19 @@ class LunaticVariable {
320
340
  public readonly name?: string;
321
341
  /** Count the number of calculation. */
322
342
  public calculatedCount = 0;
323
-
324
- constructor(
325
- args: {
326
- expression?: string;
327
- dependencies?: string[];
328
- dictionary?: Map<string, LunaticVariable>;
329
- iterationDepth?: number;
330
- shapeFrom?: string | string[];
331
- name?: string;
332
- } = {}
333
- ) {
343
+ /** Dimension **/
344
+ public dimension?: number;
345
+
346
+ constructor(args: {
347
+ expression?: string;
348
+ dependencies?: string[];
349
+ dictionary?: Map<string, LunaticVariable>;
350
+ iterationDepth?: number;
351
+ shapeFrom?: string | string[];
352
+ name?: string;
353
+ dimension?: number;
354
+ storeUpdatedAt: Timekey;
355
+ }) {
334
356
  if (args.expression && !args.dictionary) {
335
357
  throw new Error(
336
358
  `A calculated variable needs a dictionary to retrieve his deps`
@@ -343,6 +365,8 @@ class LunaticVariable {
343
365
  this.shapeFrom =
344
366
  typeof args.shapeFrom === 'string' ? [args.shapeFrom] : args.shapeFrom;
345
367
  this.name = args.name ?? args.expression;
368
+ this.dimension = args.dimension;
369
+ this.storeUpdatedAt = args.storeUpdatedAt;
346
370
  }
347
371
 
348
372
  getValue(iteration?: IterationLevel): unknown {
@@ -370,22 +394,29 @@ class LunaticVariable {
370
394
  iteration = undefined;
371
395
  }
372
396
 
373
- // Calculate bindings first to refresh "updatedAt" on calculated dependencies
374
- const bindings = this.getDependenciesValues(iteration);
375
- const hasNoBinding = Object.keys(bindings).length === 0;
397
+ const deps = this.getDependencies();
398
+ const hasNoBindings = deps.length === 0;
376
399
 
377
400
  // A static expression should not be reevaluated
378
- if (hasNoBinding && this.value) {
401
+ if (hasNoBindings && this.value) {
379
402
  return this.value;
380
403
  }
381
404
 
382
405
  // A variable without binding is a primitive (string, boolean...)
383
406
  // it yields the same results for every iteration, so we can ignore iteration
384
- if (hasNoBinding) {
407
+ if (hasNoBindings) {
385
408
  iteration = undefined;
386
409
  }
387
410
 
411
+ // The store did not change since the last calculation, skip further checks
412
+ if (this.getCalculatedAt(iteration) > this.storeUpdatedAt.getTime()) {
413
+ return this.getSavedValue(iteration);
414
+ }
415
+
416
+ // Calculate bindings first to refresh "updatedAt" on calculated dependencies
417
+ const bindings = this.getDependenciesValues(iteration);
388
418
  if (this.shapeFrom && !this.isOutdated(iteration)) {
419
+ this.updateTimestamps(iteration, 'calculatedAt');
389
420
  return this.getSavedValue(iteration);
390
421
  }
391
422
  if (isTestEnv()) {
@@ -492,6 +523,31 @@ class LunaticVariable {
492
523
  return current;
493
524
  }
494
525
 
526
+ /**
527
+ * Get a list of transitive dependencies (leaf of the dependency tree)
528
+ */
529
+ private getBaseDependencies(): string[] {
530
+ // Find the dependencies of the dependencies
531
+ const reducer = (acc: Set<string>, variableName: string) => {
532
+ if (acc.has(variableName)) {
533
+ return acc;
534
+ }
535
+ const deps = this.dictionary?.get(variableName)?.getDependencies();
536
+ if (!deps || deps.length === 0) {
537
+ acc.add(variableName);
538
+ } else {
539
+ deps?.reduce(reducer, acc);
540
+ }
541
+ return acc;
542
+ };
543
+ if (this.baseDependencies === undefined) {
544
+ this.baseDependencies = [
545
+ ...this.getDependencies().reduce(reducer, new Set<string>()),
546
+ ];
547
+ }
548
+ return this.baseDependencies;
549
+ }
550
+
495
551
  private getDependencies(): string[] {
496
552
  // Calculate dependencies from expression on the fly if necessary
497
553
  if (this.dependencies === undefined) {
@@ -534,22 +590,55 @@ class LunaticVariable {
534
590
  }
535
591
  }
536
592
 
593
+ /**
594
+ * Check if the variable should be updated (comparing calculatedAt to dependency updated time)
595
+ */
537
596
  private isOutdated(iteration?: IterationLevel): boolean {
538
- const dependenciesUpdatedAt = Math.max(
539
- 0,
540
- ...this.getDependencies().map(
541
- (dep) =>
542
- // Check when a value at the same iteration was calculated
543
- this.dictionary?.get(dep)?.updatedAt.get(iteration?.join('.')) ??
544
- // For aggregated value (max / min) look the global updatedAt time
545
- this.dictionary?.get(dep)?.updatedAt.get(undefined) ??
546
- // Otherwise this is a static value that never changes
547
- 0
548
- )
597
+ const deps = this.getDependencies();
598
+ const lastCalculatedAt = this.calculatedAt.get(iteration?.join('.'));
599
+ // Variable was never calculated
600
+ if (!lastCalculatedAt) {
601
+ return true;
602
+ }
603
+
604
+ // Look for an outdated dependency
605
+ for (const dep of deps) {
606
+ const depUpdatedAt =
607
+ this.dictionary?.get(dep)?.getUpdatedAt(iteration) ?? 0;
608
+ if (depUpdatedAt > lastCalculatedAt) {
609
+ return true;
610
+ }
611
+ }
612
+ return false;
613
+ }
614
+
615
+ public getUpdatedAt(iteration?: IterationLevel): number {
616
+ if (this.dimension === 0) {
617
+ return this.updatedAt.get(undefined) ?? 0;
618
+ }
619
+ // The value is an array, do not look at the root updatedAt if an iteration is provided
620
+ if (Array.isArray(this.value)) {
621
+ return this.updatedAt.get(iteration?.join('.')) ?? 0;
622
+ }
623
+ return (
624
+ this.updatedAt.get(iteration?.join('.')) ??
625
+ this.updatedAt.get(undefined) ??
626
+ 0
549
627
  );
628
+ }
629
+
630
+ public getCalculatedAt(iteration?: IterationLevel): number {
631
+ if (this.dimension === 0) {
632
+ return this.calculatedAt.get(undefined) ?? 0;
633
+ }
634
+ // The value is an array, do not look at the root updatedAt if an iteration is provided
635
+ if (Array.isArray(this.value)) {
636
+ return this.calculatedAt.get(iteration?.join('.')) ?? 0;
637
+ }
550
638
  return (
551
- dependenciesUpdatedAt >
552
- (this.calculatedAt.get(iteration?.join('.')) ?? -1)
639
+ this.calculatedAt.get(iteration?.join('.')) ??
640
+ this.calculatedAt.get(undefined) ??
641
+ 0
553
642
  );
554
643
  }
555
644
  }