@rkmodules/rules 0.0.170 → 0.0.172

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.
@@ -0,0 +1,58 @@
1
+ import { Tree } from "../DataTree";
2
+ import { CacheInfo, ClearCacheOptions, FunctionCall, PrimitiveFunction, RecOfTrees, RunOptions, VarRef } from "./types";
3
+ export declare const cachePath: (path: string[]) => string;
4
+ export declare function sameRecord(a: Record<string, any>, b: Record<string, any>): boolean;
5
+ /** Compare immutable graph configuration, never runtime dependency trees.
6
+ * Preserve key order (interpolation observes it); opaque objects retain identity
7
+ * semantics. Cyclic configurations conservatively miss instead of recursing forever.
8
+ */
9
+ export declare function sameConfiguration(a: any, b: any, ancestors?: Set<object>): boolean;
10
+ interface PortMemo {
11
+ raw: VarRef;
12
+ paths: string[];
13
+ dependencies: unknown[];
14
+ tree: Tree<any>;
15
+ }
16
+ interface InterpolationMemo {
17
+ ports: Map<string, PortMemo>;
18
+ result: RecOfTrees;
19
+ }
20
+ interface ResultEntry {
21
+ definition: PrimitiveFunction;
22
+ impl: PrimitiveFunction["impl"];
23
+ metadata: Record<string, any>;
24
+ inputs: RecOfTrees;
25
+ params: Record<string, any>;
26
+ result: RecOfTrees;
27
+ info: CacheInfo;
28
+ }
29
+ interface Scope {
30
+ graph: string;
31
+ cacheKey: string;
32
+ generation: number;
33
+ run: number;
34
+ interpolation: Map<string, InterpolationMemo>;
35
+ pure: Map<string, ResultEntry>;
36
+ preview: Map<string, ResultEntry>;
37
+ }
38
+ export interface CacheRun {
39
+ scope: Scope;
40
+ generation: number;
41
+ run: number;
42
+ options: RunOptions;
43
+ visited: Set<string>;
44
+ }
45
+ /** Engine-owned caches; execution state is carried explicitly across async boundaries. */
46
+ export declare class ExecutionCache {
47
+ private scopes;
48
+ begin(graph: string, options: RunOptions): CacheRun;
49
+ private current;
50
+ clear(options?: ClearCacheOptions): void;
51
+ visit(run: CacheRun, path: string[]): void;
52
+ finish(run: CacheRun): void;
53
+ interpolate(run: CacheRun, path: string[], raw: Record<string, VarRef>, scope: Record<string, RecOfTrees>, configuration?: boolean): RecOfTrees;
54
+ key(call: FunctionCall): Record<string, any>;
55
+ get(run: CacheRun, path: string[], kind: CacheInfo["kind"], fn: PrimitiveFunction, call: FunctionCall, inputs: RecOfTrees, params: Record<string, any>): ResultEntry | undefined;
56
+ set(run: CacheRun, path: string[], kind: CacheInfo["kind"], fn: PrimitiveFunction, call: FunctionCall, inputs: RecOfTrees, params: Record<string, any>, result: RecOfTrees, impl: PrimitiveFunction["impl"], createdAt: number): void;
57
+ }
58
+ export {};
@@ -1,5 +1,5 @@
1
1
  import { Tree } from "../DataTree";
2
- import { GraphedFunction, NormalizedVarDef, PrimitiveFunction, AnyFunction, VarDef, VarRef, RecOfTrees, FunctionCall, ExecutionContext } from "./types";
2
+ import { GraphedFunction, NormalizedVarDef, PrimitiveFunction, AnyFunction, VarDef, VarRef, RecOfTrees, FunctionCall, ExecutionContext, RunOptions, ClearCacheOptions, CacheInfo } from "./types";
3
3
  export * from "./types";
4
4
  export * from "./util";
5
5
  export interface Ref {
@@ -10,7 +10,13 @@ export interface Ref {
10
10
  refField: string;
11
11
  }
12
12
  export declare function normalizeVarDef(varDef: VarDef): NormalizedVarDef;
13
- export declare function getReferences(inputs: Record<string, VarRef>): Ref[];
13
+ export declare function getReferences(inputs: Record<string, VarRef>): {
14
+ name: string;
15
+ path: string;
16
+ parts: string[];
17
+ refNode: string;
18
+ refField: string;
19
+ }[];
14
20
  export declare function topSort(fn: GraphedFunction): GraphedFunction;
15
21
  export interface EngineStartEvent {
16
22
  name: string;
@@ -22,6 +28,8 @@ export interface EngineStartEvent {
22
28
  startTime: number;
23
29
  }
24
30
  export interface EngineEndEvent extends EngineStartEvent {
31
+ cached?: boolean;
32
+ cache?: CacheInfo;
25
33
  result: RecOfTrees;
26
34
  endTime: number;
27
35
  duration: number;
@@ -30,13 +38,19 @@ export type EngineEvent = EngineStartEvent | EngineEndEvent;
30
38
  export type EngineEventName = "result" | "functionCall" | "functionResult";
31
39
  export declare class Engine<Env extends Record<string, any> = Record<string, any>> {
32
40
  private fnIndex;
41
+ private graphDefinitions;
42
+ private automaticMounts;
33
43
  private listeners;
34
44
  private cache;
35
- private cacheMiss;
45
+ private graphRunners;
46
+ private executions;
47
+ private latestRuns;
36
48
  private context;
37
49
  env: Env;
38
50
  constructor(morePrimitives?: Record<string, PrimitiveFunction>, env?: Env);
39
- private checkCache;
51
+ /** Clear successful results; running calls finish but cannot refill invalidated entries. */
52
+ clearCache(options?: ClearCacheOptions): void;
53
+ private executeCall;
40
54
  private runGraph;
41
55
  /**
42
56
  * creates an function that can be evaluated by calling its implementation
@@ -45,7 +59,7 @@ export declare class Engine<Env extends Record<string, any> = Record<string, any
45
59
  * @returns
46
60
  */
47
61
  build(node: AnyFunction, id?: string): PrimitiveFunction;
48
- mount(node: PrimitiveFunction): void | (() => void) | undefined;
62
+ mount(node: PrimitiveFunction, inputs?: Record<string, VarRef>, options?: RunOptions): void | (() => void) | undefined;
49
63
  getContext(): ExecutionContext | null;
50
64
  subscribe(eventName: "result", listener: (event: EngineEndEvent) => void): () => void;
51
65
  subscribe(eventName: "functionCall", listener: (event: EngineStartEvent) => void): () => void;
@@ -53,8 +67,8 @@ export declare class Engine<Env extends Record<string, any> = Record<string, any
53
67
  fireEvent(eventName: "result", event: EngineEndEvent): void;
54
68
  fireEvent(eventName: "functionCall", event: EngineStartEvent): void;
55
69
  fireEvent(eventName: "functionResult", event: EngineEndEvent): void;
56
- run(node: AnyFunction, inputs?: Record<string, VarRef>): any;
57
- runDetailed(node: AnyFunction, inputs?: Record<string, VarRef>): Promise<EngineEndEvent>;
70
+ run(node: AnyFunction, inputs?: Record<string, VarRef>, options?: RunOptions): Promise<RecOfTrees>;
71
+ runDetailed(node: AnyFunction, inputs?: Record<string, VarRef>, options?: RunOptions): Promise<EngineEndEvent>;
58
72
  getFunction(name: string): PrimitiveFunction | undefined;
59
73
  applyNodeDelete(oldFn: GraphedFunction, nodeIds: string[]): GraphedFunction;
60
74
  applyEdgeDelete(oldFn: GraphedFunction, fromNode: string, toNode: string, fromField: string, toField: string): GraphedFunction;
@@ -34,6 +34,10 @@ export type RecOfTrees = Record<string, Tree<any>>;
34
34
  */
35
35
  export type PrimitiveFunction = {
36
36
  name: string;
37
+ /** Deterministic from immutable inputs, params and own call metadata; no side effects. */
38
+ pure?: boolean;
39
+ /** Allow stale results in explicit preview runs (e.g. external reads, never writes). */
40
+ previewCache?: boolean;
37
41
  label?: string;
38
42
  nodeType?: string | {
39
43
  name: string;
@@ -64,6 +68,10 @@ export type FunctionCall = {
64
68
  export type GraphedFunction = {
65
69
  name: string;
66
70
  label?: string;
71
+ nodeType?: string | {
72
+ name: string;
73
+ props: Record<string, any>;
74
+ };
67
75
  description?: string;
68
76
  params?: Record<string, VarDef>;
69
77
  inputs?: Record<string, VarDef>;
@@ -79,3 +87,19 @@ export interface ExecutionContext {
79
87
  timings: Record<string, number>;
80
88
  activations: Record<string, boolean>;
81
89
  }
90
+ /** Preview caching is opt-in per run; cacheKey isolates external environments/sessions. */
91
+ export interface RunOptions {
92
+ mode?: "normal" | "preview";
93
+ cacheKey?: string;
94
+ }
95
+ export interface ClearCacheOptions {
96
+ kind?: "all" | "pure" | "preview";
97
+ /** Root graph/function name, including all of its nested calls. */
98
+ graph?: string;
99
+ cacheKey?: string;
100
+ }
101
+ export interface CacheInfo {
102
+ kind: "pure" | "preview";
103
+ /** Wall-clock time at which the original successful execution completed. */
104
+ createdAt: number;
105
+ }
@@ -4,6 +4,8 @@ export declare function mergeWithOrder(...objects: Record<string, any>[]): Recor
4
4
  export declare function isReference(value: any): value is string;
5
5
  export declare function hasReference(value: any): value is string;
6
6
  export declare function parseReference(ref: string): string;
7
+ /** Reference paths using the same literal-array and token rules as interpolation. */
8
+ export declare function getReferencePaths(value: unknown): string[];
7
9
  export declare function uid(): string;
8
10
  export declare function getValue(obj: Record<string, any>, path: string): any;
9
11
  export declare function safeName(name: string): string;
@@ -1,18 +1,11 @@
1
- import { Engine, GraphedFunction } from "../Engine";
2
- /**
3
- * using functions
4
- *
5
- * in React context
6
- * - use useFunction. This will build the function, mount it if needed and provides a run function.
7
- *
8
- *
9
- * in Vanilla context
10
- * - use engine.build(fn) to build the function
11
- * - use engine.mount(fn) to mount the function (if it has a mount function)
12
- * - use await engine.run(fn, inputs) to run the function and get the result
1
+ import { Engine, GraphedFunction, RunOptions } from "../Engine";
2
+ /** Build a graph, optionally subscribe to triggers, and retain the latest run inputs.
3
+ * Preview caching requires { mode: "preview" }; refresh clears external preview
4
+ * results for this graph/session and reruns with the latest inputs.
13
5
  */
14
- export declare function useFunction(engine: Engine, fn: GraphedFunction, mount?: boolean): {
6
+ export declare function useFunction(engine: Engine, fn: GraphedFunction, mount?: boolean, options?: RunOptions): {
15
7
  fn: import("../Engine").PrimitiveFunction;
16
- run: (inputs?: Record<string, any>) => Promise<any>;
8
+ run: (inputs?: Record<string, any>) => Promise<import("../Engine").RecOfTrees>;
9
+ refresh: () => Promise<import("../Engine").RecOfTrees>;
17
10
  result: any;
18
11
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rkmodules/rules",
3
- "version": "0.0.170",
3
+ "version": "0.0.172",
4
4
  "main": "dist/index.cjs.js",
5
5
  "module": "dist/index.esm.mjs",
6
6
  "types": "./dist/index.d.ts",
package/readme.md CHANGED
@@ -278,6 +278,30 @@ in Vanilla context
278
278
  - use engine.mount(fn) to mount the function (if it has a mount function)
279
279
  - use engine.run(fn, inputs) to run the function
280
280
 
281
+ When passing a `GraphedFunction` directly to `engine.run`, treat the definition
282
+ as immutable. A new graph object replaces the registered definition even when
283
+ its name is unchanged; passing the same object again reuses its compiled
284
+ function. The engine's graph-editing helpers return new graph objects.
285
+
286
+ If you retain the compiled function returned by `engine.build`, passing that
287
+ compiled function to `engine.run` continues to run its original graph version.
288
+ Running an edited graph replaces subscriptions that the engine mounted
289
+ automatically. If you call `engine.mount` yourself, use its returned cleanup
290
+ callback to manage that subscription.
291
+
292
+ Graph compilation discovers dependencies in node inputs, including references
293
+ inside arrays, objects, data trees, and template strings. Calls execute after
294
+ their dependencies regardless of their order in `body`, including when node IDs
295
+ are numeric. The original graph is not reordered or mutated. Arrays of arrays
296
+ remain literal data, matching interpolation.
297
+
298
+ Missing referenced nodes, cycles, malformed reference paths, and unknown callable
299
+ functions are rejected before the graph is registered, mounted, or executed.
300
+ Custom functions must therefore be registered before building graphs that call
301
+ them. Output references are checked too. Runtime input names and dynamic output
302
+ fields do not require static port definitions. Node IDs must be distinct after
303
+ `safeName` escaping, and `inputs` is reserved for the graph's public input scope.
304
+
281
305
  # considerations
282
306
 
283
307
  For Supa Lipu we have quite a lot of functions that need to be called on various events
@@ -297,3 +321,160 @@ For Supa Lipu we have quite a lot of functions that need to be called on various
297
321
  # Entries
298
322
 
299
323
  - in finance I had filterLine(on, match) function that filters return a boolean if any of the lines match. This is a hardcoded solition, which is ok for finance. But we might come up with something more generic
324
+
325
+ ## Execution caching and editor previews
326
+
327
+ The engine memoizes interpolation and successful primitive results in memory.
328
+ Custom primitives execute every time unless they explicitly opt in:
329
+
330
+ ```ts
331
+ const lookupCustomer: PrimitiveFunction = {
332
+ name: "lookupCustomer",
333
+ pure: false,
334
+ previewCache: true,
335
+ impl: async ({ id }, params, engine) => ({
336
+ customer: { 0: [await engine.env.loadCustomer(id[0][0])] },
337
+ }),
338
+ };
339
+
340
+ // Normal execution: pure nodes may reuse results; external reads execute.
341
+ await engine.run(rule, inputs);
342
+
343
+ // Interactive editing: explicitly permitted external reads may also reuse results.
344
+ const options = {
345
+ mode: "preview",
346
+ cacheKey: "editor-session/customer-account",
347
+ } as const;
348
+ await engine.run(rule, inputs, options);
349
+
350
+ // Refresh external data, retaining independent pure results, then rerun.
351
+ engine.clearCache({
352
+ kind: "preview",
353
+ graph: rule.name,
354
+ cacheKey: options.cacheKey,
355
+ });
356
+ await engine.run(rule, inputs, options);
357
+
358
+ // Release all interpolation and result entries in this engine.
359
+ engine.clearCache();
360
+ ```
361
+
362
+ `pure: true` promises that the result depends only on immutable inputs, parameters,
363
+ and the node's own call metadata (including dynamic port definitions). It must not
364
+ read mutable environment state, the clock, randomness, or unrelated execution
365
+ context, and must not perform side effects. Async functions can be pure. All 67
366
+ built-in primitives have been audited: 60 are marked pure, and seven remain
367
+ explicitly impure. See the inventory below. Custom primitives remain uncached
368
+ unless they opt in.
369
+
370
+ `previewCache: true` explicitly permits stale results during preview execution.
371
+ Use it for external reads where stale values are acceptable, not writes, messages,
372
+ or other actions that must happen on every run. Preview entries have no automatic
373
+ expiry: refresh them explicitly. Normal runs never consume or replace preview-only
374
+ entries. Pure entries may be reused in either mode. Nested graph wrappers always
375
+ execute; their individual primitive children follow these policies.
376
+
377
+ Treat graph definitions, inputs, parameters, and returned trees as immutable,
378
+ including inside unmarked primitives and event listeners. Do not mutate cached
379
+ outputs or upstream inputs. Update an object/array by replacing its reference.
380
+ Graph input/output expressions, call parameters, and call metadata use `Object.is`
381
+ first, then recursive value comparison for arrays and plain objects. Reloading an
382
+ unchanged graph from JSON can therefore reuse cached results. Property and array
383
+ order remain significant; opaque objects (such as class instances) retain identity
384
+ semantics, and distinct cyclic configurations conservatively miss.
385
+ Runtime inputs, evaluated defaults, and upstream dependency trees still use identity
386
+ comparison: a new outer input map with the same port values can hit, but a newly
387
+ allocated runtime object/array value misses even if deeply equal. Unchanged graph
388
+ expressions retain their reference paths and skip repeated recursive discovery.
389
+ Function-valued graph input defaults still execute each run before their returned
390
+ values are memoized. In-place mutations are not detected.
391
+
392
+ Caches belong to an engine, root graph/function name, and optional `cacheKey`.
393
+ Nested invocation paths distinguish two uses of the same child graph. Use distinct
394
+ keys for independent editor sessions or external environments, such as different
395
+ accounts or credentials; the engine does not inspect environment changes. Change
396
+ the key or clear the relevant cache when that environment changes. Graph names are
397
+ registry identities: use distinct names for independent rules sharing an engine.
398
+ Immutable graph edits retain unchanged node entries; changed inputs, params,
399
+ implementation, or own call metadata cause misses. Removed locations are pruned
400
+ after the latest successful run.
401
+
402
+ Each scope keeps one previous successful entry per location and cache kind. There
403
+ are at most 32 recently used root/key scopes, with up to 1,024 entries each for
404
+ interpolation, pure results, and preview results. These are entry limits, not byte
405
+ limits. Large trees can still retain substantial memory. `clearCache` also accepts
406
+ `kind: "pure"` or `"all"`, plus optional `graph` and `cacheKey` filters.
407
+
408
+ Only fulfilled results are stored. Overlapping misses execute independently;
409
+ in-flight requests are not deduplicated or cancelled. An older run cannot overwrite
410
+ a newer run's entries, and clearing a scope prevents its pending runs from refilling
411
+ it. Those pending runs still return to their original callers. Invalidation is
412
+ conservative: a run already in flight cannot add further entries of either kind
413
+ in that scope. Events from overlapping runs can still arrive out of order; use
414
+ execution IDs when correlating them. `runDetailed` returns its own execution's
415
+ response directly.
416
+
417
+ Cache hits emit `functionResult` with `cached: true` and
418
+ `cache: { kind, createdAt }`, and populate the current execution context. The
419
+ `createdAt` timestamp identifies the original successful computation. Event
420
+ `duration` and context timings measure this run's work, not the original cached
421
+ computation. `functionCall` fires only when a node executes. Direct primitive runs
422
+ now also emit these events and a final `result` event. Graph result events continue
423
+ to describe the current traversal rather than a cached whole graph.
424
+
425
+ React editors can use the fourth `useFunction` argument:
426
+
427
+ ```tsx
428
+ const { run, refresh, result } = useFunction(engine, rule, true, {
429
+ mode: "preview",
430
+ cacheKey: editorSessionId,
431
+ });
432
+ // Call run(inputs) when the rule or inputs change.
433
+ // refresh() clears preview entries and reruns using this hook's latest inputs.
434
+ <button onClick={() => refresh()}>Refresh external data</button>;
435
+ ```
436
+
437
+ Calling the hook's `run()` without inputs also reuses its latest inputs. Mounted
438
+ triggers retain that hook's latest inputs and options. For non-React callers,
439
+ `engine.mount(compiled, initialInputs, options)` uses these values until a run of
440
+ that compiled function supplies newer ones. Automatic mounts similarly use the
441
+ latest run's inputs and options. The example editor and finance pages use preview
442
+ mode and expose the refresh button; generic node previews show cache origin/time.
443
+
444
+ ### Built-in purity audit
445
+
446
+ These classifications assume immutable data, parameters, and call definitions,
447
+ including deterministic property access and value conversions. Functions may
448
+ allocate objects, modify newly allocated local objects, and return references to
449
+ immutable input branches. Those operations do not introduce external side effects.
450
+
451
+ | Family | Pure functions |
452
+ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
453
+ | Math | `add`, `subtract`, `multiply`, `divide`, `modulo`, `pow`, `ceil`, `floor`, `round`, `sqrt`, `sinDeg`, `cosDeg`, `tanDeg`, `sum`, `equalTo`, `greaterThan`, `lessThan` |
454
+ | Logic | `booleanAnd`, `booleanOr`, `booleanNot`, `groupAnd`, `groupOr`, `groupStats` |
455
+ | String | `joinStrings`, `stringEqualTo`, `stringTemplate`, `toString` |
456
+ | List | `listInput`, `listItem`, `listLength`, `filterList`, `relativeItem`, `relativeItems`, `concat`, `coalesce`, `uniqueItems`, `destructureList` |
457
+ | Group | `splitGroup`, `mergeGroup`, `groupAll`, `filterTree`, `treeItem`, `simplifyTree`, `normalizeTree`, `cartesianGroups`, `mergeTree` |
458
+ | JSON | `object`, `setField`, `listToArray`, `arrayToList`, `mergeObject`, `objectEntries`, `explodeObject`, `destructureObject` |
459
+ | Sequence | `series` |
460
+ | Util | `value`, `getType`, `getValue` |
461
+ | I/O | `inputs`, `output` (their implementations return empty output maps) |
462
+ | Date | `duration` (arithmetic only) |
463
+
464
+ | Impure function | Why it remains uncached |
465
+ | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
466
+ | `log` | Console logging must occur on every evaluation. Its output tree retains the input identity, so downstream pure nodes can still hit their caches. |
467
+ | `calc` | MathJS expressions can use randomness, and failed evaluations log diagnostics. Pure expressions would need a separate restricted evaluator or eligibility policy. |
468
+ | `today` | Reads the current clock. |
469
+ | `parseDate` | Falls back to the current clock for empty input, uses host date interpretation, and logs parsing. |
470
+ | `formatDate` | Falls back to the current clock; omitted locale/timezone options depend on the host. |
471
+ | `isoDate` | Falls back to the current clock for empty input. |
472
+
473
+ `stringTemplate` reads its own dynamic input labels; these are already included
474
+ in the result cache key. `mergeObject` only mutates freshly allocated output
475
+ objects, and `setField` copies input objects before assigning fields.
476
+
477
+ Purity does not imply correctness for every input: the existing out-of-order path
478
+ normalization bug remains tracked separately, even though `normalizeTree` is
479
+ deterministic and does not mutate its inputs. Date-default and other open
480
+ regressions are likewise unchanged by this audit.