behavior-contracts 0.7.1 → 0.7.3

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.
@@ -322,6 +322,39 @@ function coveredMapNode(n, bodyIds, comp) {
322
322
  return false;
323
323
  return true;
324
324
  }
325
+ /**
326
+ * coveredFanoutNode (v3) — a FANOUT node (first-class `@refs` id-list fan-out) is covered when:
327
+ * - over is a static ref to a PRIOR BODY NODE's typed arr field OR an INPUT array port with elemType,
328
+ * - every element port is static (literal / ref into the $as binding / concat / static arr),
329
+ * - default 'fail' policy, relationKind connection (guard-checked by the guard/builder).
330
+ * The fanout de-boxes to: one deduped batched dispatch → first-seen dedupe (by the elem's dedupeKey
331
+ * field) + dangling drop + implicitSource strip + connection wrap {items, cursor:nil} — NO boxed Value.
332
+ */
333
+ function coveredFanoutNode(n, bodyIds, comp) {
334
+ if (n === null || typeof n !== "object" || !("fanout" in n))
335
+ return false;
336
+ const f = n.fanout;
337
+ if (f.policy !== undefined && f.policy !== "fail")
338
+ return false;
339
+ if (f.relationKind !== "connection")
340
+ return false;
341
+ const overE = classifyExpr(f.over);
342
+ if (overE.kind !== "ref")
343
+ return false;
344
+ const head = overE.path[0];
345
+ if (!bodyIds.has(head)) {
346
+ if (overE.path.length !== 1)
347
+ return false;
348
+ const schema = comp.inputPorts?.[head];
349
+ if (schema === undefined || schema.elemType === undefined)
350
+ return false;
351
+ }
352
+ const ports = f.ports;
353
+ for (const p of Object.keys(ports))
354
+ if (!portIsStatic(ports[p]))
355
+ return false;
356
+ return true;
357
+ }
325
358
  /** A covered read SHAPE (typing-independent) — sequential componentRef reads + covered map...into,
326
359
  * OR a real-concurrency staged plan (bc#87) whose parallel-stage members are plain componentRef point
327
360
  * reads. A real-concurrency plan whose parallel members include a map / bindField-relation child is
@@ -366,6 +399,13 @@ function coverageRejectReason(comp) {
366
399
  return `node '${n.id}' cond has no outType (required to de-box the branch join)`;
367
400
  continue;
368
401
  }
402
+ if ("fanout" in n) {
403
+ // v3: a first-class fanout node (deduped connection list-in fan-out) IS covered when its over is a
404
+ // covered ref (prior-node arr / input array with elemType) and its element ports are static.
405
+ if (!coveredFanoutNode(n, bodyIds, comp))
406
+ return `node '${n.id}' is a non-covered fanout shape (over an input array without elemType, or a dynamic element port)`;
407
+ continue;
408
+ }
369
409
  if ("map" in n) {
370
410
  // #86 pt2 batched map...into / #93 shape 2/3 / #108 guarded map (no into) / map-over-input (with
371
411
  // elemType) ARE covered. A non-covered map shape (guard+into heterogeneous / over-input without
@@ -486,6 +526,15 @@ function emitRawRowStructs(comp, typedNodes, plan) {
486
526
  if ("cond" in n)
487
527
  continue;
488
528
  const ref = typedNodes.get(n.id);
529
+ if ("fanout" in n) {
530
+ // a covered fanout node: RawElem_ is the connection items element (the per-body row the batched
531
+ // handler yields, aligned to the deduped id list); the batch RawRow_ carries the aligned []RawElem_.
532
+ const elemRowRef = fanoutItemsElemRef(n, ref, plan);
533
+ parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
534
+ parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for fanout '${n.id}': the aligned per-body rows.\n` +
535
+ `type ${rawRowStructName(comp.name, n.id)} struct {\n\tIsError bool\n\tErr string\n\tRows []${rawElemStructName(comp.name, n.id)}\n}`);
536
+ continue;
537
+ }
489
538
  if ("map" in n) {
490
539
  // a covered map node: RawElem_<comp>_<node> is the HANDLER'S per-element return shape:
491
540
  // - into: the `into` field's type (the fetched item merged onto every over element);
@@ -583,6 +632,14 @@ function emitHandlerInterface(comp, typedNodes, plan) {
583
632
  continue; // #108: a cond node has no handler method (pure Expression).
584
633
  const method = nodeMethodName(comp.name, n.id);
585
634
  const bt = boundGoType(n, comp, typedNodes, plan);
635
+ if ("fanout" in n) {
636
+ // fanout (v3): one batched dispatch of the deduped id list. Ports = the batch struct; returns the
637
+ // aligned batch row (per-body rows) + resolved. The runner applies dedupe/drop/wrap after.
638
+ const portsT = `${portsStructName(comp.name, n.id)}_batch`;
639
+ const retT = rawRowStructName(comp.name, n.id);
640
+ lines.push(`\t${method}(ports ${portsT}, bound ${bt}) (${retT}, bool)`);
641
+ continue;
642
+ }
586
643
  if ("map" in n) {
587
644
  const m = n.map;
588
645
  const batched = m.batched === true;
@@ -626,6 +683,59 @@ type ${batchStruct} struct {
626
683
  Items []${structName}
627
684
  }`;
628
685
  }
686
+ /** emitFanoutPortsStructs (v3) — the per-body element ports struct + the batch struct for a covered
687
+ * fanout node (identical shape to a batched map's ports). The element ports read the `$as` id binding. */
688
+ function emitFanoutPortsStructs(comp, node, typedNodes, plan) {
689
+ const f = node.fanout;
690
+ const structName = portsStructName(comp.name, node.id);
691
+ const { elemRef } = fanoutOverElemInfo(comp, node, typedNodes, plan);
692
+ const asBinding = { name: f.as, ref: elemRef, plan };
693
+ const elemStruct = emitPortsStruct(comp, { id: node.id, component: f.component, ports: f.ports }, asBinding, plan);
694
+ const batchStruct = `${structName}_batch`;
695
+ return `${elemStruct}
696
+
697
+ // ${batchStruct} — NATIVE batched ports for fanout '${node.id}': Items is a native slice of the concrete
698
+ // per-id ports structs (${structName}). The consumer reads Items[i].<Field> DIRECTLY (bc#90 / runtime-free).
699
+ type ${batchStruct} struct {
700
+ Items []${structName}
701
+ }`;
702
+ }
703
+ /**
704
+ * fanoutItemsElemRef (v3) — the connection items ELEMENT TypeRef for a covered fanout node. The fanout
705
+ * outType is a connection struct `{items:[]Elem, cursor:*string}`; this returns Elem (the per-body row
706
+ * the batched handler yields, aligned to the deduped id list, before dedupe/drop). Fails closed if the
707
+ * outType is not that connection shape (named struct with an `items` arr field).
708
+ */
709
+ function fanoutItemsElemRef(node, connRef, plan) {
710
+ if (connRef.kind !== "named") {
711
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go fanout (v3): node '${node.id}' outType must be a connection struct {items,cursor} (got ${JSON.stringify(connRef.kind)}).`);
712
+ }
713
+ const decl = findDecl(plan, connRef.name);
714
+ const itemsField = decl.fields.find((fd) => fd.name === "items");
715
+ if (!itemsField || itemsField.type.kind !== "arr") {
716
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go fanout (v3): node '${node.id}' connection struct '${connRef.name}' has no 'items' array field.`);
717
+ }
718
+ return itemsField.type.elem;
719
+ }
720
+ /** fanoutOverElemInfo (v3) — the over id-list element TypeRef + the native Go over-slice expression.
721
+ * Mirrors mapOverElemInfo but reads the FanoutNode.fanout spec (prior-node arr OR input array w/ elemType). */
722
+ function fanoutOverElemInfo(comp, node, typedNodes, plan) {
723
+ const f = node.fanout;
724
+ const e = classifyExpr(f.over);
725
+ if (e.kind === "ref" && e.path.length >= 1 && typedNodes.has(e.path[0])) {
726
+ const baseRef = typedNodes.get(e.path[0]);
727
+ const { expr, ref } = goTypedInternals.typedFieldAccess(typedLocal(e.path[0]), baseRef, e.path.slice(1), plan);
728
+ if (ref.kind === "arr")
729
+ return { elemRef: ref.elem, overExpr: expr };
730
+ }
731
+ if (e.kind === "ref" && e.path.length === 1) {
732
+ const schema = comp.inputPorts?.[e.path[0]];
733
+ const et = schema?.elemType;
734
+ if (et !== undefined)
735
+ return { elemRef: deriveTypeRef(et, plan), overExpr: `in.${goFieldName(e.path[0])}` };
736
+ }
737
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go fanout (v3): fanout '${node.id}' 'over' must be a ref to a prior node's typed arr field OR an input array port with a declared elemType (got ${JSON.stringify(f.over)}).`);
738
+ }
629
739
  /** mapElemRowRef — the handler's per-element RETURN shape for a covered map node: the `into` field's
630
740
  * type (into) or the element outType directly (no-into). This is the type of RawElem_<comp>_<node>. */
631
741
  function mapElemRowRef(node, arrRef, plan) {
@@ -1220,6 +1330,12 @@ function emitNativeRawRunner(comp, plan, flags) {
1220
1330
  k += stage.length - 1;
1221
1331
  continue;
1222
1332
  }
1333
+ // v3: a covered fanout node — deduped batched dispatch + first-seen dedupe + dangling drop +
1334
+ // connection wrap (the SAME definition as run_behavior's fanoutDedupDrop).
1335
+ if ("fanout" in comp.body[i]) {
1336
+ lines.push(emitFanoutArm(comp, comp.body[i], k, typedNodes, plan, priorNodeAt, zeroOut));
1337
+ continue;
1338
+ }
1223
1339
  // #86 pt2: a covered batched map...into node — struct-native element collection + into enrichment.
1224
1340
  if ("map" in comp.body[i]) {
1225
1341
  lines.push(emitMapArm(comp, comp.body[i], k, typedNodes, plan, priorNodeAt, zeroOut, flags));
@@ -1536,6 +1652,132 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
1536
1652
  function elemLocal(s) {
1537
1653
  return `oel_${s}`;
1538
1654
  }
1655
+ /**
1656
+ * emitFanoutArm (v3) — the struct-native exec of a covered FANOUT node. This is the genuinely new
1657
+ * native list-in fan-out handler: build the per-id native ports (all over ids), dispatch the batched
1658
+ * handler ONCE (aligned per-body rows), then apply THE SAME dedupe/drop as run_behavior's
1659
+ * fanoutDedupDrop — first-seen dedupe by the elem's dedupeKey field, drop dangling (a body whose
1660
+ * dedupeKey field is the zero value ≡ the interpreter's null/absent-key body), implicitSource strip
1661
+ * (STRUCTURAL — the connection items elem type already omits the implicitSource field), and wrap into
1662
+ * the connection struct {items, cursor:nil}. NO boxed Value on any plane. Byte-equal to run_behavior.
1663
+ */
1664
+ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut) {
1665
+ const f = node.fanout;
1666
+ const s = sanitize(node.id);
1667
+ const structName = portsStructName(comp.name, node.id);
1668
+ const batchStruct = `${structName}_batch`;
1669
+ const connRef = typedNodes.get(node.id);
1670
+ const elemRef = fanoutItemsElemRef(node, connRef, plan);
1671
+ const elemGo = renderTypeRef(elemRef);
1672
+ const { elemRef: overElemRef, overExpr } = fanoutOverElemInfo(comp, node, typedNodes, plan);
1673
+ const asName = f.as;
1674
+ const priorHere = (head) => priorNodeAt(head, atPos);
1675
+ const portNames = Object.keys(f.ports);
1676
+ // the dedupeKey Go field on the connection items elem (must be a string scalar for the seen-set key).
1677
+ if (elemRef.kind !== "named")
1678
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go fanout (v3): node '${node.id}' connection items element must be a named struct (got ${JSON.stringify(elemRef.kind)}).`);
1679
+ const elemDecl = findDecl(plan, elemRef.name);
1680
+ const dkField = elemDecl.fields.find((fd) => fd.name === f.dedupeKey);
1681
+ if (!dkField || dkField.type.kind !== "scalar" || dkField.type.scalar !== "string")
1682
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go fanout (v3): node '${node.id}' dedupeKey '${f.dedupeKey}' must be a string field of the connection items element type '${elemRef.name}'.`);
1683
+ const dkGo = goFieldName(f.dedupeKey);
1684
+ // per-id native ports build (same as a batched map's per-element ports). $as → the over id value.
1685
+ const buildPorts = (il, elemVar) => {
1686
+ const inits = [];
1687
+ const resolveRef = (head, restPath, opt) => {
1688
+ if (opt)
1689
+ return null;
1690
+ let baseExpr;
1691
+ let baseRef;
1692
+ if (head === asName) {
1693
+ baseExpr = elemVar;
1694
+ baseRef = overElemRef;
1695
+ }
1696
+ else if (priorHere(head)) {
1697
+ baseExpr = typedLocal(head);
1698
+ baseRef = typedNodes.get(head);
1699
+ }
1700
+ else {
1701
+ if (restPath.length !== 0)
1702
+ return null;
1703
+ const scalar = inputScalarKind(comp.inputPorts?.[head]);
1704
+ if (scalar === undefined)
1705
+ return null;
1706
+ return { expr: `in.${goFieldName(head)}`, scalar };
1707
+ }
1708
+ let acc;
1709
+ let expr;
1710
+ try {
1711
+ const r = goTypedInternals.typedFieldAccess(baseExpr, baseRef, restPath, plan);
1712
+ expr = r.expr;
1713
+ acc = r.ref;
1714
+ }
1715
+ catch {
1716
+ return null;
1717
+ }
1718
+ if (acc.kind !== "scalar" || acc.scalar === "null")
1719
+ return null;
1720
+ return { expr, scalar: goScalar(acc.scalar) };
1721
+ };
1722
+ const asBinding = { name: asName, ref: overElemRef, plan };
1723
+ for (const pn of portNames) {
1724
+ const fieldGoType = portFieldGoType(f.ports[pn], comp.inputPorts, `fanout port '${pn}' of node '${node.id}'`, asBinding, plan);
1725
+ inits.push(emitPortInit(pn, f.ports[pn], fieldGoType, resolveRef, comp.inputPorts, plan));
1726
+ }
1727
+ return [`${il}ep_${s} := ${structName}{${inits.join(", ")}}`];
1728
+ };
1729
+ const lines = [];
1730
+ const closers = [];
1731
+ lines.push(`\t// ── fanout '${node.id}' (${f.component}, dedupeKey:${JSON.stringify(f.dedupeKey)}, drop:${JSON.stringify(f.drop)}${f.implicitSource ? ", implicitSource:" + JSON.stringify(f.implicitSource) : ""}${f.parent ? ", parent:" + f.parent : ""}) ──`);
1732
+ let indent = "\t";
1733
+ if (f.parent !== undefined && priorNodeAt(f.parent, atPos)) {
1734
+ lines.push(`${indent}if produced_${sanitize(f.parent)} {`);
1735
+ closers.unshift(`${indent}}`);
1736
+ indent += "\t";
1737
+ }
1738
+ // over id-list (typed field access — no serialize) → per-id ports (all ids; dedupe is on the result).
1739
+ lines.push(`${indent}over_${s} := ${overExpr}`);
1740
+ lines.push(`${indent}var items_${s} []${elemGo}`);
1741
+ lines.push(`${indent}pi_${s} := make([]${structName}, 0, len(over_${s}))`);
1742
+ lines.push(`${indent}for _, ${elemLocal(s)} := range over_${s} {`);
1743
+ for (const l of buildPorts(indent + "\t", elemLocal(s)))
1744
+ lines.push(l);
1745
+ lines.push(`${indent}\tpi_${s} = append(pi_${s}, ep_${s})`);
1746
+ lines.push(`${indent}}`);
1747
+ // empty id-list => empty connection (handler not called), matching run_behavior.
1748
+ lines.push(`${indent}if len(pi_${s}) > 0 {`);
1749
+ lines.push(`${indent}\tbp_${s} := ${batchStruct}{Items: pi_${s}}`);
1750
+ lines.push(`${indent}\tfo_${s}, fo_${s}Resolved := handlers.${nodeMethodName(comp.name, node.id)}(bp_${s}, nil)`);
1751
+ lines.push(`${indent}\tif !fo_${s}Resolved {`, `${indent}\t\treturn ${zeroOut}, ${goErr("UNKNOWN_COMPONENT", `"component '${f.component}' has no handler (fail-closed)"`)}`, `${indent}\t}`);
1752
+ lines.push(`${indent}\tif fo_${s}.IsError {`, `${indent}\t\treturn ${zeroOut}, ${goErr("OP_FAILED", `"operation '${node.id}' failed under 'fail' policy: "+fo_${s}.Err`)}`, `${indent}\t}`);
1753
+ lines.push(`${indent}\tif len(fo_${s}.Rows) != len(pi_${s}) {`, `${indent}\t\treturn ${zeroOut}, ${goErr("FANOUT_BATCH_RESULT_MISMATCH", `fmt.Sprintf("fanout '${node.id}': batched handler must return a list aligned to the deduped id list (want %d)", len(pi_${s}))`)}`, `${indent}\t}`);
1754
+ // ── THE ONE dedupe/drop (mirrors fanoutDedupDrop verbatim): first-seen dedupe by dedupeKey field;
1755
+ // drop dangling (zero dedupeKey ≡ interpreter's null/absent-key body); implicitSource strip is
1756
+ // structural (the elem type already omits it). cursor is always nil (connection wrap).
1757
+ lines.push(`${indent}\tseen_${s} := make(map[string]struct{}, len(fo_${s}.Rows))`);
1758
+ lines.push(`${indent}\titems_${s} = make([]${elemGo}, 0, len(fo_${s}.Rows))`);
1759
+ lines.push(`${indent}\tfor fi_${s} := range fo_${s}.Rows {`);
1760
+ lines.push(`${indent}\t\tbody_${s} := fo_${s}.Rows[fi_${s}]`);
1761
+ lines.push(`${indent}\t\tvar el_${s} ${elemGo}`);
1762
+ lines.push(emitRowCopy(`el_${s}`, `body_${s}`, elemRef, plan, indent.length + 2));
1763
+ lines.push(`${indent}\t\tkey_${s} := el_${s}.${dkGo}`);
1764
+ if (f.drop === "dangling") {
1765
+ // dangling: a body whose dedupeKey is the zero value (empty string) is dropped.
1766
+ lines.push(`${indent}\t\tif key_${s} == "" {`, `${indent}\t\t\tcontinue`, `${indent}\t\t}`);
1767
+ }
1768
+ lines.push(`${indent}\t\tif _, dup_${s} := seen_${s}[key_${s}]; dup_${s} {`, `${indent}\t\t\tcontinue`, `${indent}\t\t}`);
1769
+ lines.push(`${indent}\t\tseen_${s}[key_${s}] = struct{}{}`);
1770
+ lines.push(`${indent}\t\titems_${s} = append(items_${s}, el_${s})`);
1771
+ lines.push(`${indent}\t}`);
1772
+ lines.push(`${indent}}`);
1773
+ // wrap into the connection struct {Items, Cursor:nil}. A nil items slice serializes to [] (the
1774
+ // connection serializer normalizes it) — matching {items:[],cursor:null} for an empty fan-out.
1775
+ lines.push(`${indent}${typedLocal(node.id)} = ${connRef.name}{${goFieldName("items")}: items_${s}}`);
1776
+ lines.push(`${indent}produced_${s} = true`);
1777
+ for (const c of closers)
1778
+ lines.push(c);
1779
+ return lines.join("\n");
1780
+ }
1539
1781
  /** #108: resolve an INPUT port head for a native-expr ref (cond `if` / map `when` / branches). A scalar
1540
1782
  * port → `in.<Field>` of the scalar type; an array port WITH elemType → `in.<Field>` of `[]ElemT` (so
1541
1783
  * `len(in.<Field>)` works); anything else → null (fail-closed). */
@@ -1974,7 +2216,11 @@ function emit(ctx) {
1974
2216
  for (const n of c.body)
1975
2217
  typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
1976
2218
  for (const n of c.body) {
1977
- if ("map" in n) {
2219
+ if ("fanout" in n) {
2220
+ // v3: a fanout node emits a per-id element ports struct + a batch struct (like a batched map).
2221
+ structs.push(emitFanoutPortsStructs(c, n, typedNodes, plan));
2222
+ }
2223
+ else if ("map" in n) {
1978
2224
  // #86 pt2 / #108: a map node emits an element ports struct + a batch struct. The element ports
1979
2225
  // may read the `$as` binding — resolved via the over-element type (prior node / input array).
1980
2226
  structs.push(emitMapPortsStructs(c, n, typedNodes, plan));
@@ -2002,7 +2248,7 @@ function emit(ctx) {
2002
2248
  const needSync = native.some((c) => concurrencyPlan(c) !== null);
2003
2249
  // bc#90: the ONLY std-lib `fmt.Sprintf` site on the covered plane is the batched-map result-count
2004
2250
  // mismatch error — so `fmt` is imported iff a covered component has a batched map node.
2005
- const needFmt = native.some((c) => c.body.some((n) => "map" in n && n.map.batched === true));
2251
+ const needFmt = native.some((c) => c.body.some((n) => ("map" in n && n.map.batched === true) || "fanout" in n));
2006
2252
  // The FULLY-covered module (no non-native components) gets a MINIMAL, RUNTIME-FREE header — NO
2007
2253
  // bc-runtime import at all (bc#90).
2008
2254
  // FAIL CLOSED (bc#86/#89): the --typed-native (native codegen) surface is native-only. If ANY
@@ -2114,6 +2360,39 @@ ${native.map((c) => `\t${handlerIfaceName(c.name)}`).join("\n")}
2114
2360
  if ("cond" in n)
2115
2361
  continue; // #108: cond has no handler method (pure Expression) — no scripted impl.
2116
2362
  const ref = typedNodes.get(n.id);
2363
+ if ("fanout" in n) {
2364
+ // v3: scripted fanout impl — drain one batched outcome and decode the aligned []Value into the
2365
+ // per-body elem rows (a null array element materializes to the zero elem row = dangling).
2366
+ const f = n.fanout;
2367
+ const elemRowRef = fanoutItemsElemRef(n, ref, plan);
2368
+ const elemT = rawElemStructName(c.name, n.id);
2369
+ const batchT = rawRowStructName(c.name, n.id);
2370
+ const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
2371
+ const portsT = `${portsStructName(c.name, n.id)}_batch`;
2372
+ const foBoundT = boundGoType(n, c, typedNodes, plan);
2373
+ methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${foBoundT}) (${batchT}, bool) {
2374
+ src, ok := s.next(${JSON.stringify(f.component)})
2375
+ if !ok {
2376
+ return ${batchT}{}, false
2377
+ }
2378
+ if src.isErr {
2379
+ return ${batchT}{IsError: true, Err: src.err}, true
2380
+ }
2381
+ arr, _ := src.ok.([]${PKG}.Value)
2382
+ rows := make([]${elemT}, 0, len(arr))
2383
+ for _, ev := range arr {
2384
+ // v3: a null aligned body is a DANGLING id — decode it to the zero elem row (empty dedupeKey
2385
+ // field), so the runner's dangling-drop excludes it (byte-equal to fanoutDedupDrop).
2386
+ if ev == nil {
2387
+ rows = append(rows, ${elemT}{})
2388
+ continue
2389
+ }
2390
+ rows = append(rows, ${elemDecode}(ev))
2391
+ }
2392
+ return ${batchT}{Rows: rows}, true
2393
+ }`);
2394
+ continue;
2395
+ }
2117
2396
  if ("map" in n) {
2118
2397
  const m = n.map;
2119
2398
  const batched = m.batched === true;