@lotics/app-sdk 0.115.2 → 0.115.4

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 (2) hide show
  1. package/dist/index.js +298 -122
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -716,19 +716,12 @@ function setKey(key, answer) {
716
716
  // src/written.ts
717
717
  import { useSyncExternalStore as useSyncExternalStore2 } from "react";
718
718
 
719
- // ../shared/src/add_operator.ts
720
- function addOrConcat(left, right, addNumbers) {
721
- if (typeof left === "string" || typeof right === "string") {
722
- return String(left ?? "") + String(right ?? "");
723
- }
724
- return addNumbers();
725
- }
726
-
727
719
  // ../shared/src/expression_limits.ts
728
720
  var MAX_RESULT_LENGTH = 1e5;
729
721
  var MAX_STRING_LENGTH = 1e7;
730
722
  var MAX_RANDOM_LENGTH = 1e3;
731
723
  var MAX_RUN_BUILT_BYTES = 512 * 1024 * 1024;
724
+ var MAX_IN_PLACE_BYTES = 1024 * 1024;
732
725
  var MAX_PERSISTED_JSON_LENGTH = 64 * 1024 * 1024;
733
726
  var ExpressionLimitError = class extends Error {
734
727
  constructor(message2) {
@@ -808,21 +801,54 @@ function escapedLength(text2) {
808
801
  function isLowSurrogate(code) {
809
802
  return code >= 56320 && code <= 57343;
810
803
  }
804
+ function textOf(value) {
805
+ if (!Array.isArray(value)) return ownText(value);
806
+ const parts = [];
807
+ walkJoined(value, (text2) => {
808
+ parts.push(text2);
809
+ return true;
810
+ });
811
+ return parts.join("");
812
+ }
811
813
  function textLengthUpTo(value, max3) {
812
- return textLength(value, max3, /* @__PURE__ */ new Set());
813
- }
814
- function textLength(value, max3, joining) {
815
- if (!Array.isArray(value)) return String(value).length;
816
- if (joining.has(value)) return 0;
817
- joining.add(value);
818
- let length2 = Math.max(0, value.length - 1);
819
- for (let index = 0; index < value.length && length2 <= max3; index++) {
820
- const element = value[index];
821
- if (element != null) length2 += textLength(element, max3 - length2, joining);
822
- }
823
- joining.delete(value);
814
+ if (!Array.isArray(value)) return ownText(value).length;
815
+ let length2 = 0;
816
+ walkJoined(value, (text2) => {
817
+ length2 += text2.length;
818
+ return length2 <= max3;
819
+ });
824
820
  return length2;
825
821
  }
822
+ function ownText(value) {
823
+ if (typeof value === "string") return value;
824
+ if (value instanceof Date) return Date.prototype.toString.call(value);
825
+ if (typeof value === "object" && value !== null) return Object.prototype.toString.call(value);
826
+ return String(value);
827
+ }
828
+ function walkJoined(array2, write2) {
829
+ const joining = /* @__PURE__ */ new Set([array2]);
830
+ const stack = [{ array: array2, at: 0 }];
831
+ while (stack.length > 0) {
832
+ const frame2 = stack[stack.length - 1];
833
+ if (frame2.at >= frame2.array.length) {
834
+ joining.delete(frame2.array);
835
+ stack.pop();
836
+ continue;
837
+ }
838
+ if (frame2.at > 0 && !write2(",")) return;
839
+ const element = frame2.array[frame2.at];
840
+ frame2.at += 1;
841
+ if (element === null || element === void 0) continue;
842
+ if (Array.isArray(element)) {
843
+ if (!joining.has(element)) {
844
+ joining.add(element);
845
+ stack.push({ array: element, at: 0 });
846
+ }
847
+ continue;
848
+ }
849
+ if (!write2(ownText(element))) return;
850
+ }
851
+ }
826
852
  function jsonPreview(value, max3) {
827
853
  if (typeof value === "string") {
828
854
  const text2 = JSON.stringify(value.slice(0, max3));
@@ -832,6 +858,14 @@ function jsonPreview(value, max3) {
832
858
  return Array.isArray(value) ? `[\u2026 ${value.length} items]` : "{\u2026}";
833
859
  }
834
860
 
861
+ // ../shared/src/add_operator.ts
862
+ function addOrConcat(left, right, addNumbers) {
863
+ if (typeof left === "string" || typeof right === "string") {
864
+ return textOf(left ?? "") + textOf(right ?? "");
865
+ }
866
+ return addNumbers();
867
+ }
868
+
835
869
  // ../../node_modules/jexpr/lib/constants.js
836
870
  var KEYWORDS = ["this"];
837
871
  var UNARY_OPERATORS = ["+", "-", "!"];
@@ -1691,6 +1725,16 @@ __export(expression_array_exports, {
1691
1725
  unique: () => unique,
1692
1726
  uniqueBy: () => uniqueBy
1693
1727
  });
1728
+
1729
+ // ../shared/src/own_property.ts
1730
+ function ownMember(value, key) {
1731
+ return Object.hasOwn(value, key) ? Reflect.get(value, key) : void 0;
1732
+ }
1733
+ function defineOwn(target, key, value) {
1734
+ Object.defineProperty(target, key, { value, writable: true, enumerable: true, configurable: true });
1735
+ }
1736
+
1737
+ // ../shared/src/expression_array.ts
1694
1738
  function getField(obj, field) {
1695
1739
  if (obj == null) return void 0;
1696
1740
  const parts = field.split(".");
@@ -1698,10 +1742,17 @@ function getField(obj, field) {
1698
1742
  for (const part of parts) {
1699
1743
  if (value == null) return void 0;
1700
1744
  if (typeof value !== "object") return void 0;
1701
- value = value[part];
1745
+ value = ownMember(value, part);
1702
1746
  }
1703
1747
  return value;
1704
1748
  }
1749
+ function countInto(result, key) {
1750
+ defineOwn(result, key, (Object.hasOwn(result, key) ? result[key] : 0) + 1);
1751
+ }
1752
+ function groupInto(result, key, item) {
1753
+ if (Object.hasOwn(result, key)) result[key].push(item);
1754
+ else defineOwn(result, key, [item]);
1755
+ }
1705
1756
  function compareValues(a, b) {
1706
1757
  if (a == null && b == null) return 0;
1707
1758
  if (a == null) return 1;
@@ -1863,7 +1914,7 @@ function countBy(arr, fieldOrCb) {
1863
1914
  const result2 = {};
1864
1915
  for (const val of keys2) {
1865
1916
  const key = val == null ? "null" : String(val);
1866
- result2[key] = (result2[key] || 0) + 1;
1917
+ countInto(result2, key);
1867
1918
  }
1868
1919
  return result2;
1869
1920
  });
@@ -1876,7 +1927,7 @@ function countBy(arr, fieldOrCb) {
1876
1927
  for (const item of arr) {
1877
1928
  const val = getField(item, field);
1878
1929
  const key = val == null ? "null" : String(val);
1879
- result[key] = (result[key] || 0) + 1;
1930
+ countInto(result, key);
1880
1931
  }
1881
1932
  return result;
1882
1933
  }
@@ -1996,7 +2047,7 @@ function groupBy(arr, fieldOrCb) {
1996
2047
  const result2 = {};
1997
2048
  for (let i = 0; i < arr.length; i++) {
1998
2049
  const key = keys2[i] == null ? "null" : String(keys2[i]);
1999
- (result2[key] ??= []).push(arr[i]);
2050
+ groupInto(result2, key, arr[i]);
2000
2051
  }
2001
2052
  return result2;
2002
2053
  });
@@ -2009,10 +2060,7 @@ function groupBy(arr, fieldOrCb) {
2009
2060
  for (const item of arr) {
2010
2061
  const val = getField(item, field);
2011
2062
  const key = val == null ? "null" : String(val);
2012
- if (!result[key]) {
2013
- result[key] = [];
2014
- }
2015
- result[key].push(item);
2063
+ groupInto(result, key, item);
2016
2064
  }
2017
2065
  return result;
2018
2066
  }
@@ -2946,7 +2994,7 @@ function getNestedValue(obj, path) {
2946
2994
  for (const part of parts) {
2947
2995
  if (value == null) return void 0;
2948
2996
  if (typeof value !== "object") return void 0;
2949
- value = value[part];
2997
+ value = ownMember(value, part);
2950
2998
  }
2951
2999
  return value;
2952
3000
  }
@@ -3002,9 +3050,7 @@ function pick(obj, fields) {
3002
3050
  const objRecord = obj;
3003
3051
  for (const field of fields) {
3004
3052
  const fieldStr = String(field);
3005
- if (fieldStr in objRecord) {
3006
- result[fieldStr] = objRecord[fieldStr];
3007
- }
3053
+ if (Object.hasOwn(objRecord, fieldStr)) defineOwn(result, fieldStr, objRecord[fieldStr]);
3008
3054
  }
3009
3055
  return result;
3010
3056
  }
@@ -3022,10 +3068,8 @@ function omit(obj, fields) {
3022
3068
  const result = {};
3023
3069
  const objRecord = obj;
3024
3070
  const fieldsSet = new Set(fields.map(String));
3025
- for (const key in objRecord) {
3026
- if (!fieldsSet.has(key)) {
3027
- result[key] = objRecord[key];
3028
- }
3071
+ for (const key of Object.keys(objRecord)) {
3072
+ if (!fieldsSet.has(key)) defineOwn(result, key, objRecord[key]);
3029
3073
  }
3030
3074
  return result;
3031
3075
  }
@@ -3153,7 +3197,7 @@ function toString(val) {
3153
3197
  if (typeof val !== "string" && textLengthUpTo(val, MAX_STRING_LENGTH) > MAX_STRING_LENGTH) {
3154
3198
  throw new ExpressionLimitError(`toString: result too long (max ${MAX_STRING_LENGTH} characters)`);
3155
3199
  }
3156
- return String(val);
3200
+ return textOf(val);
3157
3201
  }
3158
3202
  function isNull(val) {
3159
3203
  return val === null || val === void 0;
@@ -27341,6 +27385,12 @@ var WorkflowExpressionBudgetError = class extends Error {
27341
27385
  }
27342
27386
  budget;
27343
27387
  };
27388
+ var ExpressionNotCheapError = class extends Error {
27389
+ constructor() {
27390
+ super(`The expression walks or builds more than ${MAX_IN_PLACE_BYTES / 1024 / 1024}MB at once, past what an evaluation in place may.`);
27391
+ this.name = "ExpressionNotCheapError";
27392
+ }
27393
+ };
27344
27394
  function budgetMessage(budget) {
27345
27395
  switch (budget.exceeded) {
27346
27396
  case "bytes":
@@ -27388,6 +27438,66 @@ function builtSize(value) {
27388
27438
  if (typeof value === "object" && value !== null) return OBJECT_BYTES + Object.keys(value).length * SLOT_BYTES;
27389
27439
  return 0;
27390
27440
  }
27441
+ var LONGEST_NUMBER_TEXT = 24;
27442
+ function numberTextLength(value) {
27443
+ if (!Number.isSafeInteger(value)) return LONGEST_NUMBER_TEXT;
27444
+ let length2 = value < 0 ? 2 : 1;
27445
+ for (let rest = Math.abs(value); rest >= 10; rest = Math.floor(rest / 10)) length2 += 1;
27446
+ return length2;
27447
+ }
27448
+ function expandedSize(value, max3) {
27449
+ let bytes = 0;
27450
+ const pending = [value];
27451
+ while (pending.length > 0 && bytes <= max3) {
27452
+ const member = pending.pop();
27453
+ if (typeof member === "string") bytes += member.length * CHAR_BYTES;
27454
+ else if (typeof member === "number") bytes += numberTextLength(member) * CHAR_BYTES;
27455
+ else if (typeof member === "boolean") bytes += (member ? 4 : 5) * CHAR_BYTES;
27456
+ else if (typeof member === "bigint") bytes += String(member).length * CHAR_BYTES;
27457
+ else if (typeof member !== "object" || member === null) continue;
27458
+ else if (member instanceof Map) {
27459
+ bytes += OBJECT_BYTES + member.size * 2 * SLOT_BYTES;
27460
+ if (bytes <= max3) for (const [key, held2] of member) pending.push(key, held2);
27461
+ } else if (member instanceof Set) {
27462
+ bytes += OBJECT_BYTES + member.size * SLOT_BYTES;
27463
+ if (bytes <= max3) for (const held2 of member) pending.push(held2);
27464
+ } else if (Array.isArray(member)) {
27465
+ bytes += OBJECT_BYTES + member.length * SLOT_BYTES;
27466
+ if (bytes <= max3) for (let at2 = 0; at2 < member.length; at2++) pending.push(member[at2]);
27467
+ } else {
27468
+ const known = ownKeyBytes.get(member);
27469
+ if (known !== void 0 && bytes + known > max3) {
27470
+ bytes += known;
27471
+ continue;
27472
+ }
27473
+ const keys2 = Object.keys(member);
27474
+ let own = OBJECT_BYTES + keys2.length * SLOT_BYTES;
27475
+ for (const key of keys2) own += key.length * CHAR_BYTES;
27476
+ ownKeyBytes.set(member, own);
27477
+ bytes += own;
27478
+ if (bytes <= max3) for (const key of keys2) pending.push(Reflect.get(member, key));
27479
+ }
27480
+ }
27481
+ return bytes;
27482
+ }
27483
+ var ownKeyBytes = /* @__PURE__ */ new WeakMap();
27484
+ var OBJECT_TEXT_LENGTH = 64;
27485
+ function writtenSize(value, max3) {
27486
+ let bytes = 0;
27487
+ const pending = [value];
27488
+ while (pending.length > 0 && bytes <= max3) {
27489
+ const member = pending.pop();
27490
+ if (typeof member === "string") bytes += member.length * CHAR_BYTES;
27491
+ else if (typeof member === "number") bytes += numberTextLength(member) * CHAR_BYTES;
27492
+ else if (typeof member === "boolean") bytes += (member ? 4 : 5) * CHAR_BYTES;
27493
+ else if (typeof member === "bigint") bytes += String(member).length * CHAR_BYTES;
27494
+ else if (Array.isArray(member)) {
27495
+ bytes += OBJECT_BYTES + member.length * SLOT_BYTES;
27496
+ if (bytes <= max3) for (let at2 = 0; at2 < member.length; at2++) pending.push(member[at2]);
27497
+ } else if (typeof member === "object" && member !== null) bytes += OBJECT_TEXT_LENGTH * CHAR_BYTES;
27498
+ }
27499
+ return bytes;
27500
+ }
27391
27501
  var PARSED_NODE_BYTES = 40;
27392
27502
  var OPEN_BRACKET = "[".charCodeAt(0);
27393
27503
  var OPEN_BRACE = "{".charCodeAt(0);
@@ -27401,6 +27511,22 @@ function parsedSize(text2) {
27401
27511
  }
27402
27512
  return text2.length * CHAR_BYTES + nodes * PARSED_NODE_BYTES;
27403
27513
  }
27514
+ function assertCheap(budget, ...values2) {
27515
+ if (!budget.bounded) return;
27516
+ let bytes = 0;
27517
+ for (const value of values2) {
27518
+ bytes += writtenSize(value, MAX_IN_PLACE_BYTES - bytes);
27519
+ if (bytes > MAX_IN_PLACE_BYTES) throw new ExpressionNotCheapError();
27520
+ }
27521
+ }
27522
+ function charge(ctx, bytes) {
27523
+ const budget = ctx.evaluation_budget;
27524
+ assertUnsettled(budget);
27525
+ if (Date.now() > ctx.run_budget.deadline_ms) throw new WorkflowTimeBudgetError(ctx.run_budget.subject, ctx.run_budget.budget_ms);
27526
+ if (budget.bounded && budget.built + bytes > MAX_IN_PLACE_BYTES) throw new ExpressionNotCheapError();
27527
+ budget.built += bytes;
27528
+ chargeBuilt(ctx.run_budget, bytes);
27529
+ }
27404
27530
  function callbackWorkers() {
27405
27531
  let free = LINK_FETCH_CONCURRENCY - 1;
27406
27532
  return {
@@ -27414,10 +27540,16 @@ function callbackWorkers() {
27414
27540
  }
27415
27541
  };
27416
27542
  }
27417
- function evaluateWorkflowExpression(expr2, ctx) {
27418
- const budget = { workers: callbackWorkers(), settled: false };
27543
+ function evaluateWorkflowExpression(expr2, ctx, options = {}) {
27544
+ const budget = { workers: callbackWorkers(), bounded: options.bounded === true, built: 0, settled: false };
27419
27545
  return evaluateExpression(expr2, { ...ctx, evaluation_budget: budget }).finally(() => {
27420
27546
  budget.settled = true;
27547
+ }).then((answer) => {
27548
+ if (budget.bounded && expandedSize(answer, MAX_IN_PLACE_BYTES) > MAX_IN_PLACE_BYTES) throw new ExpressionNotCheapError();
27549
+ return answer;
27550
+ }).catch((error51) => {
27551
+ if (budget.bounded && ctx.run_budget.host === "shared") ctx.run_budget.built_bytes -= budget.built;
27552
+ throw error51;
27421
27553
  });
27422
27554
  }
27423
27555
  async function evaluateExpression(expr2, ctx) {
@@ -27443,15 +27575,13 @@ async function evaluateExpression(expr2, ctx) {
27443
27575
  return evaluateLambda(expr2.params, expr2.body, ctx, []);
27444
27576
  case "array_literal": {
27445
27577
  const elements = await Promise.all(expr2.elements.map((el) => evaluateExpression(el, ctx)));
27446
- chargeBuilt(ctx.run_budget, builtSize(elements));
27578
+ charge(ctx, builtSize(elements));
27447
27579
  return elements;
27448
27580
  }
27449
27581
  case "object_literal": {
27450
27582
  const out = {};
27451
- for (const entry of expr2.entries) {
27452
- out[entry.key] = await evaluateExpression(entry.value, ctx);
27453
- }
27454
- chargeBuilt(ctx.run_budget, builtSize(out));
27583
+ for (const entry of expr2.entries) defineOwn(out, entry.key, await evaluateExpression(entry.value, ctx));
27584
+ charge(ctx, builtSize(out));
27455
27585
  return out;
27456
27586
  }
27457
27587
  default:
@@ -27472,9 +27602,7 @@ function assertUnsettled(budget) {
27472
27602
  function evaluateLambda(params, body, ctx, items) {
27473
27603
  const call = async (...args) => {
27474
27604
  const frame2 = {};
27475
- for (let i = 0; i < params.length; i++) {
27476
- frame2[params[i]] = args[i];
27477
- }
27605
+ for (let i = 0; i < params.length; i++) defineOwn(frame2, params[i], args[i]);
27478
27606
  const stack = ctx.lambda_params ?? [];
27479
27607
  const newStack = [...stack, frame2];
27480
27608
  const innerCtx = {
@@ -27542,16 +27670,14 @@ function resolveReadRoot(source, ctx) {
27542
27670
  case "foreach_index":
27543
27671
  return resolveForeachFrame(ctx, void 0, "foreach_index").index;
27544
27672
  case "step_output": {
27545
- if (!(source.step_id in ctx.step_outputs)) {
27673
+ if (!Object.hasOwn(ctx.step_outputs, source.step_id)) {
27546
27674
  throw new WorkflowEvalError(
27547
27675
  `Read from step_output "${source.step_id}", but no such step has run`,
27548
27676
  { kind: "read", at: "step_output", extra: { step_id: source.step_id } }
27549
27677
  );
27550
27678
  }
27551
27679
  const stored = ctx.step_outputs[source.step_id];
27552
- if (stored && typeof stored === "object" && "output" in stored) {
27553
- return stored.output;
27554
- }
27680
+ if (stored && typeof stored === "object" && Object.hasOwn(stored, "output")) return ownMember(stored, "output");
27555
27681
  return stored;
27556
27682
  }
27557
27683
  case "runtime":
@@ -27561,7 +27687,7 @@ function resolveReadRoot(source, ctx) {
27561
27687
  if (stack !== void 0) {
27562
27688
  for (let i = stack.length - 1; i >= 0; i--) {
27563
27689
  const frame2 = stack[i];
27564
- if (source.name in frame2) return frame2[source.name];
27690
+ if (Object.hasOwn(frame2, source.name)) return frame2[source.name];
27565
27691
  }
27566
27692
  }
27567
27693
  throw new WorkflowEvalError(
@@ -27574,7 +27700,7 @@ function resolveReadRoot(source, ctx) {
27574
27700
  if (stack !== void 0) {
27575
27701
  for (let i = stack.length - 1; i >= 0; i--) {
27576
27702
  const frame2 = stack[i];
27577
- if (source.name in frame2) return frame2[source.name];
27703
+ if (Object.hasOwn(frame2, source.name)) return frame2[source.name];
27578
27704
  }
27579
27705
  }
27580
27706
  throw new WorkflowEvalError(
@@ -27676,63 +27802,74 @@ async function coerceFieldDisplay(value, field, ctx) {
27676
27802
  if (value === null || value === void 0) return value;
27677
27803
  switch (field.type) {
27678
27804
  case "select":
27679
- return coerceSelectDisplay(value, field.options);
27805
+ return coerceSelectDisplay(value, field.options, ctx);
27680
27806
  // One of its own options, named as a select's is.
27681
27807
  case "formula":
27682
- return field.formula.options === void 0 ? value : coerceSelectDisplay(value, field.formula.options);
27808
+ return field.formula.options === void 0 ? value : coerceSelectDisplay(value, field.formula.options, ctx);
27683
27809
  case "select_record_link":
27684
27810
  return coerceRecordLinkDisplay(value, field, ctx);
27685
27811
  case "select_member":
27686
27812
  return coerceMemberDisplay(value, ctx);
27687
27813
  case "lookup":
27688
- return field.lookup_field_options ? coerceOptionKeysDeep(value, field.lookup_field_options) : value;
27814
+ return field.lookup_field_options ? coerceOptionKeysDeep(value, field.lookup_field_options, ctx) : value;
27689
27815
  default:
27690
27816
  return value;
27691
27817
  }
27692
27818
  }
27693
- function coerceSelectDisplay(value, options) {
27819
+ function coerceSelectDisplay(value, options, ctx) {
27694
27820
  const optionMap = new Map(options.map((o) => [o.key, o.name]));
27695
27821
  const resolve = (key) => {
27696
27822
  if (typeof key !== "string") return "";
27697
27823
  return optionMap.get(key) ?? key;
27698
27824
  };
27699
- if (Array.isArray(value)) return value.map(resolve);
27700
- return resolve(value);
27825
+ if (!Array.isArray(value)) return resolve(value);
27826
+ assertCheap(ctx.evaluation_budget, value);
27827
+ const names = value.map(resolve);
27828
+ charge(ctx, builtSize(names));
27829
+ return names;
27701
27830
  }
27702
- function coerceOptionKeysDeep(value, options) {
27831
+ function coerceOptionKeysDeep(value, options, ctx) {
27832
+ assertCheap(ctx.evaluation_budget, value);
27703
27833
  const optionMap = new Map(options.map((o) => [o.key, o.name]));
27704
27834
  const resolve = (v) => {
27705
- if (Array.isArray(v)) return v.map(resolve);
27835
+ if (Array.isArray(v)) {
27836
+ const resolved = v.map(resolve);
27837
+ charge(ctx, builtSize(resolved));
27838
+ return resolved;
27839
+ }
27706
27840
  if (typeof v === "string") return optionMap.get(v) ?? v;
27707
27841
  return v;
27708
27842
  };
27709
27843
  return resolve(value);
27710
27844
  }
27711
27845
  async function coerceMemberDisplay(value, ctx) {
27846
+ if (Array.isArray(value)) assertCheap(ctx.evaluation_budget, value);
27712
27847
  const ids = Array.isArray(value) ? value.filter((v) => typeof v === "string") : typeof value === "string" ? [value] : [];
27713
27848
  if (ids.length === 0) return Array.isArray(value) ? [] : "";
27714
27849
  const nameMap = await ctx.member_lookup(ids);
27715
27850
  const names = new Set(nameMap.values());
27716
27851
  const resolve = (id) => nameMap.get(id) ?? (names.has(id) ? id : "");
27717
- return Array.isArray(value) ? value.map((v) => resolve(String(v))) : resolve(ids[0]);
27852
+ if (!Array.isArray(value)) return resolve(ids[0]);
27853
+ const shown = value.map((v) => resolve(textOf(v)));
27854
+ charge(ctx, builtSize(shown));
27855
+ return shown;
27718
27856
  }
27719
27857
  async function coerceRecordLinkDisplay(value, field, ctx) {
27720
27858
  if (!Array.isArray(value)) return value;
27859
+ assertCheap(ctx.evaluation_budget, value);
27721
27860
  const displayFieldKeys = field.display_field_keys ?? [];
27722
- return Promise.all(
27723
- value.map(async (id) => {
27724
- if (typeof id !== "string") return "";
27725
- const data = await fetchRecordData(id, ctx);
27726
- if (!data) return "";
27727
- if (displayFieldKeys.length === 0) {
27728
- return "";
27729
- }
27730
- return displayFieldKeys.map((key) => {
27731
- const v = data[key];
27732
- return v === null || v === void 0 ? "" : String(v);
27733
- }).filter((s) => s !== "").join(" ");
27734
- })
27861
+ const rows = await fetchRecordsBounded(
27862
+ value.map((id) => typeof id === "string" ? id : null),
27863
+ `Showing "${field.key}"`,
27864
+ ctx
27735
27865
  );
27866
+ const shown = rows.map((row2) => row2 === null ? [] : displayFieldKeys.map((key) => row2[key]));
27867
+ assertCheap(ctx.evaluation_budget, shown);
27868
+ const texts = shown.map(
27869
+ (values2) => values2.filter((v) => v !== null && v !== void 0).map(textOf).filter((text2) => text2 !== "").join(" ")
27870
+ );
27871
+ charge(ctx, texts.reduce((bytes, text2) => bytes + builtSize(text2), builtSize(texts)));
27872
+ return texts;
27736
27873
  }
27737
27874
  function advanceTable(segment, currentTable, ctx) {
27738
27875
  if (segment.at === "linked" && currentTable) {
@@ -27762,7 +27899,7 @@ async function walkSegment(value, segment, ctx) {
27762
27899
  return void 0;
27763
27900
  }
27764
27901
  if (typeof value !== "object") return void 0;
27765
- return value[segment.key];
27902
+ return ownMember(value, segment.key);
27766
27903
  }
27767
27904
  case "index": {
27768
27905
  if (value === null || value === void 0) return void 0;
@@ -27772,7 +27909,7 @@ async function walkSegment(value, segment, ctx) {
27772
27909
  }
27773
27910
  if (typeof value === "object") {
27774
27911
  const key = typeof segment.index === "number" ? String(segment.index) : await resolveObjectKeyExpression(segment.index, ctx);
27775
- return value[key];
27912
+ return ownMember(value, key);
27776
27913
  }
27777
27914
  throw new WorkflowEvalError(
27778
27915
  `Cannot index ${typeof value} \u2014 only an array or an object can be read by index.`,
@@ -27790,9 +27927,8 @@ async function resolveObjectKeyExpression(expr2, ctx) {
27790
27927
  throw new WorkflowEvalError(
27791
27928
  // Bounded: the value is whatever the expression produced — a whole record,
27792
27929
  // a page of rows — and the message goes into a persisted execution row and
27793
- // an author-facing error. The type is what names the fault; a prefix is
27794
- // enough to recognize which value it was.
27795
- `Computed object key must evaluate to a string, got ${value === null ? "null" : typeof value}: ${jsonPreview(value, VALUE_PREVIEW_LENGTH)}`,
27930
+ // an author-facing error.
27931
+ `Computed object key must evaluate to a string, got ${describeValue(value)}`,
27796
27932
  { kind: "read", at: "index" }
27797
27933
  );
27798
27934
  }
@@ -27801,7 +27937,7 @@ async function resolveIndexExpression(expr2, ctx, context) {
27801
27937
  const value = await evaluateExpression(expr2, ctx);
27802
27938
  if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
27803
27939
  throw new WorkflowEvalError(
27804
- `Computed ${context} must evaluate to a non-negative integer, got ${value === null ? "null" : typeof value}: ${jsonPreview(value, VALUE_PREVIEW_LENGTH)}`,
27940
+ `Computed ${context} must evaluate to a non-negative integer, got ${describeValue(value)}`,
27805
27941
  { kind: "read", at: "index" }
27806
27942
  );
27807
27943
  }
@@ -27815,17 +27951,17 @@ async function walkLinked(value, segment, ctx) {
27815
27951
  { kind: "read", at: "linked", extra: { via: segment.via } }
27816
27952
  );
27817
27953
  }
27818
- const linkValue = value[segment.via];
27819
- const ids = extractLinkIds(linkValue, segment.via);
27954
+ const ids = linkIds(ownMember(value, segment.via), segment.via);
27820
27955
  if (segment.index === "each") {
27821
- return fetchRecordsBounded(ids, segment.via, ctx);
27956
+ assertCheap(ctx.evaluation_budget, ids);
27957
+ return fetchRecordsBounded(ids.map((id) => assertLinkId(id, segment.via)), `linked(...) on "${segment.via}"`, ctx);
27822
27958
  }
27823
27959
  const idx = typeof segment.index === "number" ? segment.index : await resolveIndexExpression(segment.index, ctx, `linked index via "${segment.via}"`);
27824
27960
  const target = ids[idx];
27825
27961
  if (target === void 0) return void 0;
27826
- return fetchRecordData(target, ctx);
27962
+ return fetchRecordData(assertLinkId(target, segment.via), ctx);
27827
27963
  }
27828
- function extractLinkIds(value, via) {
27964
+ function linkIds(value, via) {
27829
27965
  if (value === void 0 || value === null) return [];
27830
27966
  if (!Array.isArray(value)) {
27831
27967
  throw new WorkflowEvalError(
@@ -27833,16 +27969,17 @@ function extractLinkIds(value, via) {
27833
27969
  { kind: "read", at: "linked", extra: { via } }
27834
27970
  );
27835
27971
  }
27836
- for (const item of value) {
27837
- if (typeof item !== "string") {
27838
- throw new WorkflowEvalError(
27839
- `Linked field "${via}" contains a non-string item \u2014 record data must be normalized before evaluation`,
27840
- { kind: "read", at: "linked", extra: { via, item_type: typeof item } }
27841
- );
27842
- }
27843
- }
27844
27972
  return value;
27845
27973
  }
27974
+ function assertLinkId(item, via) {
27975
+ if (typeof item !== "string") {
27976
+ throw new WorkflowEvalError(
27977
+ `Linked field "${via}" contains a non-string item \u2014 record data must be normalized before evaluation`,
27978
+ { kind: "read", at: "linked", extra: { via, item_type: typeof item } }
27979
+ );
27980
+ }
27981
+ return item;
27982
+ }
27846
27983
  async function fetchRecordData(recordId, ctx) {
27847
27984
  if (ctx.record_cache.has(recordId)) {
27848
27985
  return ctx.record_cache.get(recordId) ?? null;
@@ -27854,18 +27991,18 @@ async function fetchRecordData(recordId, ctx) {
27854
27991
  }
27855
27992
  var LINK_FETCH_CONCURRENCY = 20;
27856
27993
  var MAX_LINK_FAN_OUT = 1e4;
27857
- async function fetchRecordsBounded(ids, via, ctx) {
27994
+ async function fetchRecordsBounded(ids, reading, ctx) {
27858
27995
  if (ids.length > MAX_LINK_FAN_OUT) {
27859
27996
  throw new WorkflowEvalError(
27860
- `linked(...) on "${via}" would fetch ${ids.length} rows, over the cap of ${MAX_LINK_FAN_OUT}. Index one row \u2014 linked(...)[i] \u2014 or narrow the link.`,
27861
- { kind: "read", at: "linked", extra: { via } }
27997
+ `${reading} would fetch ${ids.length} rows, over the cap of ${MAX_LINK_FAN_OUT}. Index one row \u2014 linked(...)[i] \u2014 or narrow the link.`,
27998
+ { kind: "read", at: "linked", extra: { reading } }
27862
27999
  );
27863
28000
  }
27864
28001
  const results = [];
27865
28002
  for (let i = 0; i < ids.length; i += LINK_FETCH_CONCURRENCY) {
27866
28003
  for (let pause = checkClock(ctx); pause !== null; pause = checkClock(ctx)) await pause;
27867
28004
  const chunk = ids.slice(i, i + LINK_FETCH_CONCURRENCY);
27868
- results.push(...await Promise.all(chunk.map((id) => fetchRecordData(id, ctx))));
28005
+ results.push(...await Promise.all(chunk.map((id) => id === null ? null : fetchRecordData(id, ctx))));
27869
28006
  }
27870
28007
  return results;
27871
28008
  }
@@ -27885,12 +28022,12 @@ async function evaluateCall(fn, args, ctx) {
27885
28022
  );
27886
28023
  for (let pause = checkClock(ctx); pause !== null; pause = checkClock(ctx)) await pause;
27887
28024
  const cost = helperCost(fn);
27888
- if (cost === "parses") chargeBuilt(ctx.run_budget, parsedSize(evaluatedArgs[0]));
28025
+ if (cost === "parses") charge(ctx, parsedSize(evaluatedArgs[0]));
27889
28026
  const answer = await helper(...evaluatedArgs);
27890
28027
  if (cost !== "builds" && cost !== "nested") return answer;
27891
28028
  const handedBack = evaluatedArgs.indexOf(answer);
27892
28029
  if (handedBack !== -1) return evaluatedArgs[handedBack];
27893
- chargeBuilt(ctx.run_budget, builtSize(answer) + (cost === "nested" ? builtSize(evaluatedArgs[0]) : 0));
28030
+ charge(ctx, builtSize(answer) + (cost === "nested" ? builtSize(evaluatedArgs[0]) : 0));
27894
28031
  return answer;
27895
28032
  }
27896
28033
  async function evaluateBinary(op, leftExpr, rightExpr, ctx) {
@@ -27911,9 +28048,9 @@ async function evaluateBinary(op, leftExpr, rightExpr, ctx) {
27911
28048
  for (let pause = checkClock(ctx); pause !== null; pause = checkClock(ctx)) await pause;
27912
28049
  switch (op) {
27913
28050
  case "eq":
27914
- return looseEq(left, right);
27915
28051
  case "neq":
27916
- return !looseEq(left, right);
28052
+ assertCheap(ctx.evaluation_budget, looselyComparedText(left, right));
28053
+ return looseEq(left, right) === (op === "eq");
27917
28054
  case "seq":
27918
28055
  return strictEq(left, right);
27919
28056
  case "sneq":
@@ -27928,10 +28065,11 @@ async function evaluateBinary(op, leftExpr, rightExpr, ctx) {
27928
28065
  return compareOrdered(left, right) <= 0;
27929
28066
  case "add": {
27930
28067
  if (typeof left === "string" || typeof right === "string") {
28068
+ assertCheap(ctx.evaluation_budget, left, right);
27931
28069
  assertTextWithin("+", typeof left === "string" ? null : left, typeof right === "string" ? null : right);
27932
28070
  }
27933
28071
  const sum2 = addOrConcat(left, right, () => assertNumber(left, "add", "left") + assertNumber(right, "add", "right"));
27934
- chargeBuilt(ctx.run_budget, builtSize(sum2));
28072
+ charge(ctx, builtSize(sum2));
27935
28073
  return sum2;
27936
28074
  }
27937
28075
  case "sub":
@@ -27955,8 +28093,21 @@ async function evaluateBinary(op, leftExpr, rightExpr, ctx) {
27955
28093
  }
27956
28094
  }
27957
28095
  function looseEq(a, b) {
28096
+ if (isComposite(a) && b !== null && b !== void 0 && !isComposite(b)) return looseEq(textOf(a), b);
28097
+ if (isComposite(b) && a !== null && a !== void 0 && !isComposite(a)) return looseEq(a, textOf(b));
27958
28098
  return a == b;
27959
28099
  }
28100
+ function isComposite(value) {
28101
+ return typeof value === "object" && value !== null;
28102
+ }
28103
+ function looselyComparedText(a, b) {
28104
+ if (Array.isArray(a) && isScalar(b)) return a;
28105
+ if (Array.isArray(b) && isScalar(a)) return b;
28106
+ return null;
28107
+ }
28108
+ function isScalar(value) {
28109
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint";
28110
+ }
27960
28111
  function strictEq(a, b) {
27961
28112
  return a === b;
27962
28113
  }
@@ -27990,8 +28141,14 @@ function describeValue(value) {
27990
28141
  if (typeof value === "string") return `string ${jsonPreview(value, VALUE_PREVIEW_LENGTH)}`;
27991
28142
  if (typeof value === "number") return `number ${String(value)}`;
27992
28143
  if (typeof value === "boolean") return `boolean ${String(value)}`;
27993
- const kind = Array.isArray(value) ? "array" : typeof value;
27994
- return textLengthUpTo(value, VALUE_PREVIEW_LENGTH) <= VALUE_PREVIEW_LENGTH ? `${kind} ${String(value)}` : kind;
28144
+ if (!Array.isArray(value)) return typeof value;
28145
+ let length2 = 0;
28146
+ for (let at2 = 0; at2 < value.length && length2 <= VALUE_PREVIEW_LENGTH; at2++) {
28147
+ const element = value[at2];
28148
+ if (element !== null && element !== void 0 && !isScalar(element)) return "array";
28149
+ length2 += (typeof element === "string" ? element.length : LONGEST_NUMBER_TEXT) + 1;
28150
+ }
28151
+ return value.length > 0 && length2 <= VALUE_PREVIEW_LENGTH ? `array ${value.join(",")}` : "array";
27995
28152
  }
27996
28153
  async function evaluateUnary(op, argExpr, ctx) {
27997
28154
  const value = await evaluateExpression(argExpr, ctx);
@@ -28013,9 +28170,10 @@ async function evaluateTemplate(parts, ctx) {
28013
28170
  parts.map((part) => part.t === "text" ? part.s : evaluateExpression(part.e, ctx))
28014
28171
  );
28015
28172
  for (let pause = checkClock(ctx); pause !== null; pause = checkClock(ctx)) await pause;
28173
+ assertCheap(ctx.evaluation_budget, ...evaluated2);
28016
28174
  assertTextWithin("template", ...evaluated2);
28017
28175
  const text2 = evaluated2.map(stringifyForTemplate).join("");
28018
- chargeBuilt(ctx.run_budget, builtSize(text2));
28176
+ charge(ctx, builtSize(text2));
28019
28177
  return text2;
28020
28178
  }
28021
28179
  function assertTextWithin(writer, ...values2) {
@@ -28029,8 +28187,7 @@ function assertTextWithin(writer, ...values2) {
28029
28187
  }
28030
28188
  function stringifyForTemplate(value) {
28031
28189
  if (value === null || value === void 0) return "";
28032
- if (typeof value === "string") return value;
28033
- return String(value);
28190
+ return textOf(value);
28034
28191
  }
28035
28192
 
28036
28193
  // ../shared/src/id.ts
@@ -28090,6 +28247,11 @@ function knownCells(tables, tableId, cells) {
28090
28247
  },
28091
28248
  has(target, key) {
28092
28249
  return isField(key) || Reflect.has(target, key);
28250
+ },
28251
+ // An expression reads only what a row holds itself (`ownMember`).
28252
+ getOwnPropertyDescriptor(target, key) {
28253
+ if (isField(key) && !Object.hasOwn(target, key)) unread(key);
28254
+ return Reflect.getOwnPropertyDescriptor(target, key);
28093
28255
  }
28094
28256
  }
28095
28257
  );
@@ -28099,6 +28261,11 @@ function modelled(stepId, output) {
28099
28261
  get(target, key) {
28100
28262
  if (typeof key === "string" && key !== "then" && !(key in target)) unread(`${stepId}.${key}`);
28101
28263
  return Reflect.get(target, key);
28264
+ },
28265
+ // An expression reads only what an output holds itself (`ownMember`).
28266
+ getOwnPropertyDescriptor(target, key) {
28267
+ if (typeof key === "string" && key !== "then" && !Object.hasOwn(target, key)) unread(`${stepId}.${key}`);
28268
+ return Reflect.getOwnPropertyDescriptor(target, key);
28102
28269
  }
28103
28270
  });
28104
28271
  }
@@ -31353,33 +31520,37 @@ function buildDependencyGraph(formulaFields, allFields) {
31353
31520
  }
31354
31521
  return { sortedFields: result, circularDependencies };
31355
31522
  }
31356
- function readsFieldsOnlyThroughEmptinessPredicates(node) {
31523
+ function readsFieldsOnlyThroughEmptinessPredicates(node, taken) {
31524
+ const reads = (child) => readsFieldsOnlyThroughEmptinessPredicates(child, taken);
31357
31525
  if (node === void 0) return true;
31358
31526
  if (isFieldRead(node)) return false;
31359
31527
  switch (node.type) {
31360
31528
  case "Invoke": {
31361
31529
  const args = node.arguments ?? [];
31362
31530
  const predicate = node.method === void 0 && node.receiver.type === "ID" && EMPTINESS_PREDICATES.has(node.receiver.value);
31363
- return (predicate || readsFieldsOnlyThroughEmptinessPredicates(node.receiver)) && args.every(
31364
- (arg) => predicate && isFieldRead(arg) || readsFieldsOnlyThroughEmptinessPredicates(arg)
31531
+ return (predicate || reads(node.receiver)) && args.every(
31532
+ (arg) => predicate && isFieldRead(arg) || reads(arg)
31365
31533
  );
31366
31534
  }
31367
31535
  case "Unary":
31368
- return readsFieldsOnlyThroughEmptinessPredicates(node.child);
31536
+ return reads(node.child);
31369
31537
  case "Binary":
31370
- return readsFieldsOnlyThroughEmptinessPredicates(node.left) && readsFieldsOnlyThroughEmptinessPredicates(node.right);
31538
+ return reads(node.left) && reads(node.right);
31371
31539
  case "Getter":
31372
- return readsFieldsOnlyThroughEmptinessPredicates(node.receiver);
31540
+ return reads(node.receiver);
31373
31541
  case "Index":
31374
- return readsFieldsOnlyThroughEmptinessPredicates(node.receiver) && readsFieldsOnlyThroughEmptinessPredicates(node.argument);
31375
- case "Ternary":
31376
- return readsFieldsOnlyThroughEmptinessPredicates(node.condition) && readsFieldsOnlyThroughEmptinessPredicates(node.trueExpr) && readsFieldsOnlyThroughEmptinessPredicates(node.falseExpr);
31542
+ return reads(node.receiver) && reads(node.argument);
31543
+ case "Ternary": {
31544
+ if (!reads(node.condition)) return false;
31545
+ const decided = node.condition === void 0 || taken === void 0 ? void 0 : taken(node.condition);
31546
+ return decided === true ? reads(node.trueExpr) : decided === false ? reads(node.falseExpr) : reads(node.trueExpr) && reads(node.falseExpr);
31547
+ }
31377
31548
  case "List":
31378
- return (node.items ?? []).every(readsFieldsOnlyThroughEmptinessPredicates);
31549
+ return (node.items ?? []).every((item) => reads(item));
31379
31550
  case "Map":
31380
- return Object.values(node.entries ?? {}).every(readsFieldsOnlyThroughEmptinessPredicates);
31551
+ return Object.values(node.entries ?? {}).every((entry) => reads(entry));
31381
31552
  case "ArrowFunction":
31382
- return readsFieldsOnlyThroughEmptinessPredicates(node.body);
31553
+ return reads(node.body);
31383
31554
  case "ID":
31384
31555
  return node.value !== "data";
31385
31556
  case "Literal":
@@ -31392,8 +31563,13 @@ function isFieldRead(node) {
31392
31563
  }
31393
31564
  function evaluateFormulaWithJS(preparedExpression, data, timezone, everyReferenceEmpty) {
31394
31565
  try {
31395
- if (everyReferenceEmpty && !readsFieldsOnlyThroughEmptinessPredicates(parseExpression(preparedExpression))) {
31396
- return null;
31566
+ if (everyReferenceEmpty) {
31567
+ const helpers = createTimezoneAwareFunctions(timezone);
31568
+ const taken = (condition2) => {
31569
+ const answer = condition2.evaluate({ ...data, ...helpers });
31570
+ return typeof answer === "boolean" ? answer : void 0;
31571
+ };
31572
+ if (!readsFieldsOnlyThroughEmptinessPredicates(parseExpression(preparedExpression), taken)) return null;
31397
31573
  }
31398
31574
  const result = evaluateJsExpression(preparedExpression, data, void 0, timezone);
31399
31575
  if (result === null || result === void 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/app-sdk",
3
- "version": "0.115.2",
3
+ "version": "0.115.4",
4
4
  "description": "The SDK a Lotics custom-code app reads and writes through \u2014 typed hooks over the host bridge, cell readers, mount() and AppRouter",
5
5
  "type": "module",
6
6
  "exports": {