@zapier/kitcore 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @zapier/kitcore
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 615fda9: The resolution controller now paginates dynamic selects as a page window: each `select` ask carries ONE page of choices plus a `page: { generation, index }` descriptor, so payloads stay proportional to a page.
8
+ - `next_page` and `previous_page` actions replace `more`.
9
+ - State carries coordinates only: `ControllerPagination` holds the cursor trail, never items, so the host carries cursors back between steps instead of the items it was just sent. An accumulating host appends pages client-side keyed on the question's `path` + `generation`, which increments whenever a `search` restarts the listing.
10
+ - Every question carries `path` (which field it is for), so hosts never read the state.
11
+ - Resolver `validate` is now a top-level callback (`validate({ imports, value, input, context })`), async and able to verify against the source; a thrown validate surfaces as a `failed` result with retry, not a rejection. A `validate` inside the prompt config is a type error (`ResolverPromptConfig` omits it), so it can't silently never run.
12
+ - The `exhausted` flag is removed; end-of-list is the absence of `next_page`.
13
+ - A failed fetch records its position so `retry` replays exactly the page that failed.
14
+
15
+ - e6b22fb: `concatPaginated` now paginates with stateless cursors: each source is called with its own cursor per page (`({ cursor }) => ...`), outgoing cursors encode the source index plus that source's cursor, and a new `cursor` option resumes the stream from a previous page's cursor with no shared in-memory state. The `dedupe` option is removed; sources must be disjoint by construction.
16
+
3
17
  ## 0.6.0
4
18
 
5
19
  ### Minor Changes
package/dist/index.cjs CHANGED
@@ -301,7 +301,8 @@ function getCoreErrorCause(value) {
301
301
  var CURSOR_VERSION = 1;
302
302
  var CURSOR_SOURCE = {
303
303
  API: "api",
304
- SDK: "sdk"
304
+ SDK: "sdk",
305
+ CONCAT: "concat"
305
306
  };
306
307
  function encodeBase64(str) {
307
308
  return btoa(
@@ -521,10 +522,32 @@ async function* paginateBuffered(pageFunction, pageOptions) {
521
522
  }
522
523
  }
523
524
  var paginate = paginateBuffered;
525
+ function encodeConcatCursor(index, cursor) {
526
+ const envelope = {
527
+ v: CURSOR_VERSION,
528
+ source: CURSOR_SOURCE.CONCAT,
529
+ index,
530
+ cursor
531
+ };
532
+ return encodeBase64(JSON.stringify(envelope));
533
+ }
534
+ function decodeConcatCursor(incoming) {
535
+ if (!incoming) {
536
+ return { index: 0, cursor: void 0 };
537
+ }
538
+ try {
539
+ const envelope = JSON.parse(decodeBase64(incoming));
540
+ if (envelope.v === CURSOR_VERSION && envelope.source === CURSOR_SOURCE.CONCAT && typeof envelope.index === "number") {
541
+ return { index: envelope.index, cursor: envelope.cursor };
542
+ }
543
+ } catch {
544
+ }
545
+ return { index: 0, cursor: incoming };
546
+ }
524
547
  function concatPaginated({
525
548
  sources,
526
- dedupe,
527
- pageSize = 100
549
+ pageSize = 100,
550
+ cursor
528
551
  }) {
529
552
  if (sources.length === 0) {
530
553
  const empty = { data: [] };
@@ -534,40 +557,24 @@ function concatPaginated({
534
557
  }
535
558
  });
536
559
  }
537
- let sourceIndex = 0;
538
- let currentIterator = null;
539
- const seen = /* @__PURE__ */ new Set();
540
- const pageFunction = async (_options) => {
541
- while (sourceIndex < sources.length) {
542
- if (!currentIterator) {
543
- const result = sources[sourceIndex]();
544
- currentIterator = result[Symbol.asyncIterator]();
545
- }
546
- const next = await currentIterator.next();
547
- if (next.done) {
548
- sourceIndex++;
549
- currentIterator = null;
560
+ const pageFunction = async (options) => {
561
+ let { index, cursor: sourceCursor } = decodeConcatCursor(options.cursor);
562
+ while (index < sources.length) {
563
+ const page = await sources[index]({ cursor: sourceCursor });
564
+ const hasMoreInSource = page.nextCursor != null;
565
+ if (page.data.length === 0 && !hasMoreInSource) {
566
+ index++;
567
+ sourceCursor = void 0;
550
568
  continue;
551
569
  }
552
- let items = next.value.data;
553
- if (dedupe) {
554
- if (sourceIndex > 0) {
555
- items = items.filter((item) => !seen.has(dedupe(item)));
556
- }
557
- for (const item of items) {
558
- seen.add(dedupe(item));
559
- }
560
- }
561
- const hasMoreInSource = next.value.nextCursor != null;
562
- const hasMoreSources = sourceIndex < sources.length - 1;
563
570
  return {
564
- data: items,
565
- nextCursor: hasMoreInSource || hasMoreSources ? "__has_more__" : void 0
571
+ data: page.data,
572
+ nextCursor: hasMoreInSource ? encodeConcatCursor(index, page.nextCursor) : index < sources.length - 1 ? encodeConcatCursor(index + 1, void 0) : void 0
566
573
  };
567
574
  }
568
575
  return { data: [] };
569
576
  };
570
- const iterator = paginateBuffered(pageFunction, { pageSize });
577
+ const iterator = paginateBuffered(pageFunction, { pageSize, cursor });
571
578
  const firstPagePromise = iterator.next().then((result) => {
572
579
  if (result.done) {
573
580
  return { data: [] };
@@ -1448,6 +1455,7 @@ function defineResolver(config) {
1448
1455
  getContext: config.getContext,
1449
1456
  listItems: config.listItems,
1450
1457
  prompt: config.prompt,
1458
+ validate: config.validate,
1451
1459
  tryResolveWithoutPrompt: config.tryResolveWithoutPrompt,
1452
1460
  tryResolveFromSearch: config.tryResolveFromSearch
1453
1461
  };
@@ -2106,6 +2114,7 @@ function bindResolver(resolver, plugins) {
2106
2114
  const {
2107
2115
  getContext: getContext2,
2108
2116
  listItems,
2117
+ validate,
2109
2118
  tryResolveWithoutPrompt,
2110
2119
  tryResolveFromSearch
2111
2120
  } = resolver;
@@ -2119,6 +2128,9 @@ function bindResolver(resolver, plugins) {
2119
2128
  };
2120
2129
  if (getContext2)
2121
2130
  bound.getContext = ({ input }) => getContext2({ imports, input });
2131
+ if (validate) {
2132
+ bound.validate = ({ value, input, context }) => validate({ imports, value, input, context });
2133
+ }
2122
2134
  if (tryResolveWithoutPrompt) {
2123
2135
  bound.tryResolveWithoutPrompt = ({ input }) => tryResolveWithoutPrompt({ imports, input });
2124
2136
  }
@@ -2798,14 +2810,14 @@ function coerce(leaf, raw) {
2798
2810
  }
2799
2811
  async function validationError(leaf, value, state) {
2800
2812
  if (leaf.resolver?.type !== "dynamic") return null;
2813
+ const validate = leaf.resolver.validate;
2814
+ if (!validate) return null;
2801
2815
  const context = await resolveContext(leaf, state.resolved);
2802
- const config = leaf.resolver.prompt?.({
2803
- items: state.listing?.items ?? [],
2816
+ const verdict = await validate({
2817
+ value,
2804
2818
  input: mergeInput(state.resolved, leaf.extraInput),
2805
2819
  context
2806
2820
  });
2807
- if (!config?.validate) return null;
2808
- const verdict = config.validate(value);
2809
2821
  if (verdict === true) return null;
2810
2822
  return typeof verdict === "string" ? verdict : `${leaf.name}: invalid value`;
2811
2823
  }
@@ -2931,20 +2943,36 @@ async function resolveContext(leaf, input) {
2931
2943
  input: mergeInput(input, leaf.extraInput)
2932
2944
  });
2933
2945
  }
2934
- async function fetchListing(leaf, input, opts = {}) {
2946
+ function firstPagePosition(opts = {}) {
2947
+ return {
2948
+ ...opts.search !== void 0 ? { search: opts.search } : {},
2949
+ pageCursor: null,
2950
+ previousCursors: [],
2951
+ generation: opts.generation ?? 0
2952
+ };
2953
+ }
2954
+ async function fetchListing(leaf, input, position, context) {
2955
+ if (leaf.resolver?.type === "dynamic" && leaf.resolver.inputType === "search" && position.search === void 0) {
2956
+ return { position, items: [] };
2957
+ }
2935
2958
  const page = await firstPage(
2936
2959
  leaf.resolver?.type === "dynamic" ? leaf.resolver.listItems({
2937
2960
  input: mergeInput(input, leaf.extraInput),
2938
- context: opts.context,
2939
- search: opts.search,
2940
- cursor: opts.cursor
2961
+ context,
2962
+ search: position.search,
2963
+ cursor: position.pageCursor ?? void 0
2941
2964
  }) : void 0
2942
2965
  );
2943
2966
  return {
2944
- items: [...opts.priorItems ?? [], ...page.data],
2945
- cursor: page.nextCursor,
2946
- search: opts.search,
2947
- exhausted: page.nextCursor == null
2967
+ position,
2968
+ items: page.data,
2969
+ ...page.nextCursor != null ? { nextCursor: page.nextCursor } : {}
2970
+ };
2971
+ }
2972
+ function toPagination(page) {
2973
+ return {
2974
+ position: page.position,
2975
+ ...page.nextCursor !== void 0 ? { nextCursor: page.nextCursor } : {}
2948
2976
  };
2949
2977
  }
2950
2978
 
@@ -2966,23 +2994,32 @@ var AFFORDANCE = {
2966
2994
  description: "Filter the options by a search term",
2967
2995
  supply: "term"
2968
2996
  },
2969
- more: { action: "more", description: "Load more options" },
2997
+ nextPage: {
2998
+ action: "next_page",
2999
+ description: "Fetch the next page of options"
3000
+ },
3001
+ previousPage: {
3002
+ action: "previous_page",
3003
+ description: "Return to the previous page of options"
3004
+ },
2970
3005
  skip: { action: "skip", description: "Omit this optional parameter" },
2971
3006
  add: { action: "add", description: "Add another item" },
2972
3007
  done: { action: "done", description: "Finish the list" },
2973
3008
  retry: { action: "retry", description: "Retry loading the options" },
2974
3009
  cancel: { action: "cancel", description: "Cancel resolution" }
2975
3010
  };
2976
- function selectActions(leaf, listing, multiple) {
3011
+ function selectActions(leaf, page, multiple) {
2977
3012
  const searchMode = leaf.resolver?.type === "dynamic" && leaf.resolver.inputType === "search";
2978
- if (searchMode && listing.search === void 0 && listing.items.length === 0) {
3013
+ if (searchMode && page.position.search === void 0 && page.items.length === 0) {
2979
3014
  const actions2 = multiple ? [AFFORDANCE.search] : [AFFORDANCE.search, AFFORDANCE.custom];
2980
3015
  if (!leaf.required) actions2.push(AFFORDANCE.skip);
2981
3016
  return actions2;
2982
3017
  }
2983
3018
  const actions = multiple ? [AFFORDANCE.choose] : [AFFORDANCE.choose, AFFORDANCE.custom];
2984
3019
  if (searchMode) actions.push(AFFORDANCE.search);
2985
- if (listing.cursor) actions.push(AFFORDANCE.more);
3020
+ if (page.nextCursor) actions.push(AFFORDANCE.nextPage);
3021
+ if (page.position.previousCursors.length > 0)
3022
+ actions.push(AFFORDANCE.previousPage);
2986
3023
  if (!leaf.required) actions.push(AFFORDANCE.skip);
2987
3024
  return actions;
2988
3025
  }
@@ -2990,16 +3027,17 @@ function labeledMessage(leaf) {
2990
3027
  if (!leaf.label) return void 0;
2991
3028
  return `${leaf.label} (${leaf.required ? "required" : "optional"}):`;
2992
3029
  }
2993
- function selectQuestion(leaf, input, listing, context) {
3030
+ function selectQuestion(leaf, path, input, page, context) {
2994
3031
  const resolver = leaf.resolver?.type === "dynamic" ? leaf.resolver : void 0;
2995
3032
  const config = resolver?.prompt?.({
2996
- items: listing.items,
3033
+ items: page.items,
2997
3034
  input: mergeInput(input, leaf.extraInput),
2998
3035
  context
2999
3036
  });
3000
3037
  const multiple = config?.type === "checkbox";
3001
3038
  return {
3002
3039
  type: "select",
3040
+ path,
3003
3041
  // A labeled field's title beats the resolver's message: per-field
3004
3042
  // resolvers are shared across fields (one choices-fetcher for every
3005
3043
  // field), so only the leaf knows which field is being asked.
@@ -3007,9 +3045,13 @@ function selectQuestion(leaf, input, listing, context) {
3007
3045
  choices: (config?.choices ?? []).map(toChoice),
3008
3046
  ...multiple ? { multiple: true } : {},
3009
3047
  ...config?.notes?.length ? { notes: config.notes } : {},
3010
- ...listing.search !== void 0 ? { search: listing.search } : {},
3048
+ ...page.position.search !== void 0 ? { search: page.position.search } : {},
3011
3049
  ...resolver?.placeholder ? { placeholder: resolver.placeholder } : {},
3012
- actions: selectActions(leaf, listing, multiple)
3050
+ page: {
3051
+ generation: page.position.generation,
3052
+ index: page.position.previousCursors.length
3053
+ },
3054
+ actions: selectActions(leaf, page, multiple)
3013
3055
  };
3014
3056
  }
3015
3057
  function toControllerError(error) {
@@ -3031,6 +3073,7 @@ function failedResult(state, name, error) {
3031
3073
  error: toControllerError(error),
3032
3074
  question: {
3033
3075
  type: "select",
3076
+ path: state.current ?? [],
3034
3077
  message: `Could not load options for ${name}.`,
3035
3078
  choices: [],
3036
3079
  actions: [AFFORDANCE.retry, AFFORDANCE.cancel]
@@ -3038,27 +3081,15 @@ function failedResult(state, name, error) {
3038
3081
  }
3039
3082
  };
3040
3083
  }
3041
- async function buildQuestion(leaf, input) {
3084
+ async function buildQuestion(leaf, path, input) {
3042
3085
  const optional = !leaf.required;
3043
3086
  const resolver = leaf.resolver;
3044
3087
  if (resolver?.type === "dynamic") {
3045
3088
  const context = await resolveContext(leaf, input);
3046
- if (resolver.inputType === "search") {
3047
- const listing2 = {
3048
- items: [],
3049
- cursor: void 0,
3050
- search: void 0,
3051
- exhausted: true
3052
- };
3053
- return {
3054
- question: selectQuestion(leaf, input, listing2, context),
3055
- listing: listing2
3056
- };
3057
- }
3058
- const listing = await fetchListing(leaf, input, { context });
3089
+ const page = await fetchListing(leaf, input, firstPagePosition(), context);
3059
3090
  return {
3060
- question: selectQuestion(leaf, input, listing, context),
3061
- listing
3091
+ question: selectQuestion(leaf, path, input, page, context),
3092
+ pagination: toPagination(page)
3062
3093
  };
3063
3094
  }
3064
3095
  if (leaf.staticChoices) {
@@ -3067,6 +3098,7 @@ async function buildQuestion(leaf, input) {
3067
3098
  return {
3068
3099
  question: {
3069
3100
  type: "select",
3101
+ path,
3070
3102
  message: `Select ${leaf.name}:`,
3071
3103
  choices: leaf.staticChoices,
3072
3104
  actions: actions2
@@ -3080,6 +3112,7 @@ async function buildQuestion(leaf, input) {
3080
3112
  return {
3081
3113
  question: {
3082
3114
  type: "input",
3115
+ path,
3083
3116
  // The optional marker makes Enter-to-pass discoverable on a bare
3084
3117
  // parameter; a labeled field carries its marker via labeledMessage.
3085
3118
  message: labeledMessage(leaf) ?? `Enter ${leaf.name}${optional ? " (optional)" : ""}:`,
@@ -3094,6 +3127,7 @@ function collectionQuestion(t) {
3094
3127
  if (t.count >= t.min) actions.push(AFFORDANCE.done);
3095
3128
  return {
3096
3129
  type: "collection",
3130
+ path: t.path,
3097
3131
  message: `Add ${t.path[t.path.length - 1]}[${t.count}]?`,
3098
3132
  container: "array",
3099
3133
  count: t.count,
@@ -3107,6 +3141,7 @@ function collectionQuestion(t) {
3107
3141
  function objectGateQuestion(path) {
3108
3142
  return {
3109
3143
  type: "collection",
3144
+ path,
3110
3145
  message: `Add ${path[path.length - 1]}?`,
3111
3146
  container: "object",
3112
3147
  actions: [
@@ -3118,9 +3153,10 @@ function objectGateQuestion(path) {
3118
3153
  ]
3119
3154
  };
3120
3155
  }
3121
- function optionalsGateQuestion(pending) {
3156
+ function optionalsGateQuestion(path, pending) {
3122
3157
  return {
3123
3158
  type: "collection",
3159
+ path,
3124
3160
  // The prompt and its context ride separately so a host renders the info
3125
3161
  // line above the confirm without composing any text of its own.
3126
3162
  message: "Would you like to configure optional fields?",
@@ -3270,8 +3306,12 @@ async function askLeaf(state, path, leaf, opts = {}) {
3270
3306
  state.current = path;
3271
3307
  delete state.gate;
3272
3308
  try {
3273
- const { question, listing } = await buildQuestion(leaf, state.resolved);
3274
- state.listing = listing;
3309
+ const { question, pagination } = await buildQuestion(
3310
+ leaf,
3311
+ path,
3312
+ state.resolved
3313
+ );
3314
+ state.pagination = pagination;
3275
3315
  return {
3276
3316
  state,
3277
3317
  result: {
@@ -3281,7 +3321,7 @@ async function askLeaf(state, path, leaf, opts = {}) {
3281
3321
  }
3282
3322
  };
3283
3323
  } catch (error) {
3284
- state.listing = { items: [], exhausted: false };
3324
+ state.pagination = failedPagination(void 0, firstPagePosition());
3285
3325
  return failedResult(state, leaf.name, error);
3286
3326
  }
3287
3327
  }
@@ -3291,13 +3331,13 @@ async function advance(ctx, state) {
3291
3331
  if (!target) {
3292
3332
  delete state.current;
3293
3333
  delete state.gate;
3294
- delete state.listing;
3334
+ delete state.pagination;
3295
3335
  return { state, result: finalize(ctx, state.resolved) };
3296
3336
  }
3297
3337
  if (target.kind === "array") {
3298
3338
  state.current = target.path;
3299
3339
  state.gate = "array";
3300
- delete state.listing;
3340
+ delete state.pagination;
3301
3341
  return {
3302
3342
  state,
3303
3343
  result: { status: "ask", question: collectionQuestion(target) }
@@ -3306,7 +3346,7 @@ async function advance(ctx, state) {
3306
3346
  if (target.kind === "object") {
3307
3347
  state.current = target.path;
3308
3348
  state.gate = "entry";
3309
- delete state.listing;
3349
+ delete state.pagination;
3310
3350
  return {
3311
3351
  state,
3312
3352
  result: { status: "ask", question: objectGateQuestion(target.path) }
@@ -3315,12 +3355,12 @@ async function advance(ctx, state) {
3315
3355
  if (target.kind === "optionals") {
3316
3356
  state.current = target.path;
3317
3357
  state.gate = "optionals";
3318
- delete state.listing;
3358
+ delete state.pagination;
3319
3359
  return {
3320
3360
  state,
3321
3361
  result: {
3322
3362
  status: "ask",
3323
- question: optionalsGateQuestion(target.pending)
3363
+ question: optionalsGateQuestion(target.path, target.pending)
3324
3364
  }
3325
3365
  };
3326
3366
  }
@@ -3371,13 +3411,13 @@ async function step(ctx, prior, action) {
3371
3411
  if (action.type === "cancel") {
3372
3412
  delete state.current;
3373
3413
  delete state.gate;
3374
- delete state.listing;
3414
+ delete state.pagination;
3375
3415
  return { state, result: { status: "cancelled" } };
3376
3416
  }
3377
3417
  const path = state.current;
3378
3418
  if (!path) throw new Error("step called with no outstanding question");
3379
3419
  const leaf = await leafAt(ctx, path, state.resolved);
3380
- if (leaf && (action.type === "search" || action.type === "more" || action.type === "retry")) {
3420
+ if (leaf && (action.type === "search" || action.type === "next_page" || action.type === "previous_page" || action.type === "retry")) {
3381
3421
  return refine(ctx, state, leaf, path, action);
3382
3422
  }
3383
3423
  if (action.type === "add" || action.type === "done") {
@@ -3389,7 +3429,7 @@ async function step(ctx, prior, action) {
3389
3429
  }
3390
3430
  delete state.current;
3391
3431
  delete state.gate;
3392
- delete state.listing;
3432
+ delete state.pagination;
3393
3433
  if (action.type === "done") {
3394
3434
  settle(state, path);
3395
3435
  return advance(ctx, state);
@@ -3418,23 +3458,44 @@ async function step(ctx, prior, action) {
3418
3458
  case "choose":
3419
3459
  case "custom": {
3420
3460
  if (leaf) {
3421
- const error = await validationError(leaf, action.value, state);
3461
+ let error;
3462
+ try {
3463
+ error = await validationError(leaf, action.value, state);
3464
+ } catch (thrown) {
3465
+ return failedResult(state, leaf.name, thrown);
3466
+ }
3422
3467
  if (error) {
3423
- if (state.listing && leaf.resolver?.type === "dynamic") {
3424
- const context = await resolveContext(leaf, state.resolved);
3425
- return {
3426
- state,
3427
- result: {
3428
- status: "ask",
3429
- question: selectQuestion(
3430
- leaf,
3431
- state.resolved,
3432
- state.listing,
3433
- context
3434
- ),
3435
- error
3436
- }
3437
- };
3468
+ if (state.pagination && leaf.resolver?.type === "dynamic") {
3469
+ try {
3470
+ const context = await resolveContext(leaf, state.resolved);
3471
+ const page = await fetchListing(
3472
+ leaf,
3473
+ state.resolved,
3474
+ state.pagination.position,
3475
+ context
3476
+ );
3477
+ state.pagination = toPagination(page);
3478
+ return {
3479
+ state,
3480
+ result: {
3481
+ status: "ask",
3482
+ question: selectQuestion(
3483
+ leaf,
3484
+ path,
3485
+ state.resolved,
3486
+ page,
3487
+ context
3488
+ ),
3489
+ error
3490
+ }
3491
+ };
3492
+ } catch (fetchError) {
3493
+ state.pagination = failedPagination(
3494
+ state.pagination,
3495
+ state.pagination.position
3496
+ );
3497
+ return failedResult(state, leaf.name, fetchError);
3498
+ }
3438
3499
  }
3439
3500
  return askLeaf(state, path, leaf, { error });
3440
3501
  }
@@ -3453,15 +3514,11 @@ async function step(ctx, prior, action) {
3453
3514
  throw new Error(`action "${action.type}" is not supported here`);
3454
3515
  }
3455
3516
  delete state.current;
3456
- delete state.listing;
3517
+ delete state.pagination;
3457
3518
  return advance(ctx, state);
3458
3519
  }
3459
3520
  async function refine(ctx, state, leaf, path, action) {
3460
- const attempt = action.type === "search" ? { search: action.term, cursor: void 0, priorItems: [] } : {
3461
- search: state.listing?.search,
3462
- cursor: state.listing?.cursor,
3463
- priorItems: state.listing?.items ?? []
3464
- };
3521
+ const position = positionAfter(state.pagination, action);
3465
3522
  try {
3466
3523
  if (action.type === "search") {
3467
3524
  const exact = leaf.resolver?.type === "dynamic" ? await leaf.resolver.tryResolveFromSearch?.({
@@ -3471,32 +3528,62 @@ async function refine(ctx, state, leaf, path, action) {
3471
3528
  if (exact) {
3472
3529
  setAtPath(state.resolved, path, coerce(leaf, exact.resolvedValue));
3473
3530
  delete state.current;
3474
- delete state.listing;
3531
+ delete state.pagination;
3475
3532
  return advance(ctx, state);
3476
3533
  }
3477
3534
  }
3478
3535
  const context = await resolveContext(leaf, state.resolved);
3479
- state.listing = await fetchListing(leaf, state.resolved, {
3480
- ...attempt,
3481
- context
3482
- });
3536
+ const page = await fetchListing(leaf, state.resolved, position, context);
3537
+ state.pagination = toPagination(page);
3483
3538
  return {
3484
3539
  state,
3485
3540
  result: {
3486
3541
  status: "ask",
3487
- question: selectQuestion(leaf, state.resolved, state.listing, context)
3542
+ question: selectQuestion(leaf, path, state.resolved, page, context)
3488
3543
  }
3489
3544
  };
3490
3545
  } catch (error) {
3491
- state.listing = {
3492
- items: attempt.priorItems,
3493
- search: attempt.search,
3494
- cursor: attempt.cursor,
3495
- exhausted: false
3496
- };
3546
+ state.pagination = failedPagination(state.pagination, position);
3497
3547
  return failedResult(state, leaf.name, error);
3498
3548
  }
3499
3549
  }
3550
+ function failedPagination(pagination, retryPosition) {
3551
+ return {
3552
+ position: pagination?.position ?? firstPagePosition(),
3553
+ ...pagination?.nextCursor !== void 0 ? { nextCursor: pagination.nextCursor } : {},
3554
+ retryPosition
3555
+ };
3556
+ }
3557
+ function positionAfter(pagination, action) {
3558
+ const current = pagination?.position ?? firstPagePosition();
3559
+ switch (action.type) {
3560
+ case "search":
3561
+ return firstPagePosition({
3562
+ search: action.term,
3563
+ generation: current.generation + 1
3564
+ });
3565
+ case "next_page": {
3566
+ if (pagination?.nextCursor == null) return current;
3567
+ return {
3568
+ ...current.search !== void 0 ? { search: current.search } : {},
3569
+ pageCursor: pagination.nextCursor,
3570
+ previousCursors: [...current.previousCursors, current.pageCursor],
3571
+ generation: current.generation
3572
+ };
3573
+ }
3574
+ case "previous_page": {
3575
+ if (current.previousCursors.length === 0) return current;
3576
+ return {
3577
+ ...current.search !== void 0 ? { search: current.search } : {},
3578
+ pageCursor: current.previousCursors[current.previousCursors.length - 1],
3579
+ previousCursors: current.previousCursors.slice(0, -1),
3580
+ generation: current.generation
3581
+ };
3582
+ }
3583
+ case "retry":
3584
+ return pagination?.retryPosition ?? current;
3585
+ }
3586
+ }
3500
3587
 
3501
3588
  // src/model/resolution/controller.ts
3502
3589
  function toJsonSchema(schema) {