@zapier/kitcore 0.6.0 → 0.8.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,29 @@
1
1
  # @zapier/kitcore
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - cec12cd: Added `concatLists`, which lists one page of several paginated lists joined end to end, returning a plain promise of `{ data, nextCursor }`. Pass a page's `nextCursor` to a fresh call to resume the concatenation.
8
+
9
+ Deprecated `concatPaginated` in favor of `concatLists`. It now delegates to `concatLists` and logs a deprecation warning; awaiting it yields the same page, but the page-iterable half of its old return shape is removed.
10
+
11
+ - 769751e: Paginated list results now expose `.pages()`, a plain async iterable over pages. Unlike iterating the result directly (which still works but is deprecated), `.pages()` is not also a promise, so returning it from an `async` function no longer silently collapses it to the first page. `.items()` is unchanged and remains the way to iterate individual items across pages. The `toIterable()` helper is deprecated in favor of `.pages()` and now logs a deprecation warning.
12
+
13
+ ## 0.7.0
14
+
15
+ ### Minor Changes
16
+
17
+ - 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.
18
+ - `next_page` and `previous_page` actions replace `more`.
19
+ - 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.
20
+ - Every question carries `path` (which field it is for), so hosts never read the state.
21
+ - 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.
22
+ - The `exhausted` flag is removed; end-of-list is the absence of `next_page`.
23
+ - A failed fetch records its position so `retry` replays exactly the page that failed.
24
+
25
+ - 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.
26
+
3
27
  ## 0.6.0
4
28
 
5
29
  ### Minor Changes
package/dist/index.cjs CHANGED
@@ -31,6 +31,7 @@ __export(index_exports, {
31
31
  CoreSignal: () => CoreSignal,
32
32
  addPlugin: () => addPlugin,
33
33
  composePlugins: () => composePlugins,
34
+ concatLists: () => concatLists,
34
35
  concatPaginated: () => concatPaginated,
35
36
  coreOptionsPluginRef: () => coreOptionsPluginRef,
36
37
  createAsyncContext: () => createAsyncContext,
@@ -301,7 +302,8 @@ function getCoreErrorCause(value) {
301
302
  var CURSOR_VERSION = 1;
302
303
  var CURSOR_SOURCE = {
303
304
  API: "api",
304
- SDK: "sdk"
305
+ SDK: "sdk",
306
+ CONCAT: "concat"
305
307
  };
306
308
  function encodeBase64(str) {
307
309
  return btoa(
@@ -521,69 +523,71 @@ async function* paginateBuffered(pageFunction, pageOptions) {
521
523
  }
522
524
  }
523
525
  var paginate = paginateBuffered;
524
- function concatPaginated({
526
+ function encodeConcatCursor(index, cursor) {
527
+ const envelope = {
528
+ v: CURSOR_VERSION,
529
+ source: CURSOR_SOURCE.CONCAT,
530
+ index,
531
+ cursor
532
+ };
533
+ return encodeBase64(JSON.stringify(envelope));
534
+ }
535
+ function decodeConcatCursor(incoming) {
536
+ if (!incoming) {
537
+ return { index: 0, cursor: void 0 };
538
+ }
539
+ try {
540
+ const envelope = JSON.parse(decodeBase64(incoming));
541
+ if (envelope.v === CURSOR_VERSION && envelope.source === CURSOR_SOURCE.CONCAT && typeof envelope.index === "number") {
542
+ return { index: envelope.index, cursor: envelope.cursor };
543
+ }
544
+ } catch {
545
+ }
546
+ return { index: 0, cursor: incoming };
547
+ }
548
+ async function concatLists({
525
549
  sources,
526
- dedupe,
527
- pageSize = 100
550
+ pageSize = 100,
551
+ cursor
528
552
  }) {
529
553
  if (sources.length === 0) {
530
- const empty = { data: [] };
531
- return Object.assign(Promise.resolve(empty), {
532
- [Symbol.asyncIterator]: async function* () {
533
- yield empty;
534
- }
535
- });
554
+ return { data: [] };
536
555
  }
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;
556
+ const pageFunction = async (options) => {
557
+ let { index, cursor: listCursor } = decodeConcatCursor(options.cursor);
558
+ while (index < sources.length) {
559
+ const page = await sources[index]({ cursor: listCursor });
560
+ const hasMoreInList = page.nextCursor != null;
561
+ if (page.data.length === 0 && !hasMoreInList) {
562
+ index++;
563
+ listCursor = void 0;
550
564
  continue;
551
565
  }
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
566
  return {
564
- data: items,
565
- nextCursor: hasMoreInSource || hasMoreSources ? "__has_more__" : void 0
567
+ data: page.data,
568
+ nextCursor: hasMoreInList ? encodeConcatCursor(index, page.nextCursor) : index < sources.length - 1 ? encodeConcatCursor(index + 1, void 0) : void 0
566
569
  };
567
570
  }
568
571
  return { data: [] };
569
572
  };
570
- const iterator = paginateBuffered(pageFunction, { pageSize });
571
- const firstPagePromise = iterator.next().then((result) => {
572
- if (result.done) {
573
- return { data: [] };
574
- }
575
- return result.value;
576
- });
577
- return Object.assign(firstPagePromise, {
578
- [Symbol.asyncIterator]: async function* () {
579
- yield await firstPagePromise;
580
- for await (const page of { [Symbol.asyncIterator]: () => iterator }) {
581
- yield page;
582
- }
583
- }
584
- });
573
+ const result = await paginateBuffered(pageFunction, {
574
+ pageSize,
575
+ cursor
576
+ }).next();
577
+ return result.done ? { data: [] } : result.value;
578
+ }
579
+ function concatPaginated({
580
+ sources,
581
+ pageSize,
582
+ cursor
583
+ }) {
584
+ logDeprecation("concatPaginated() is deprecated. Use concatLists() instead.");
585
+ return concatLists({ sources, pageSize, cursor });
585
586
  }
586
587
  function toIterable(source) {
588
+ logDeprecation(
589
+ "toIterable() is deprecated. Call .pages() on the paginated result instead."
590
+ );
587
591
  return { [Symbol.asyncIterator]: () => source[Symbol.asyncIterator]() };
588
592
  }
589
593
 
@@ -936,6 +940,13 @@ function createPaginatedFunction(coreFn, options) {
936
940
  [Symbol.asyncIterator]() {
937
941
  return pageStream;
938
942
  },
943
+ pages: function() {
944
+ return {
945
+ [Symbol.asyncIterator]() {
946
+ return pageStream;
947
+ }
948
+ };
949
+ },
939
950
  items: function() {
940
951
  return {
941
952
  [Symbol.asyncIterator]: async function* () {
@@ -1448,6 +1459,7 @@ function defineResolver(config) {
1448
1459
  getContext: config.getContext,
1449
1460
  listItems: config.listItems,
1450
1461
  prompt: config.prompt,
1462
+ validate: config.validate,
1451
1463
  tryResolveWithoutPrompt: config.tryResolveWithoutPrompt,
1452
1464
  tryResolveFromSearch: config.tryResolveFromSearch
1453
1465
  };
@@ -2106,6 +2118,7 @@ function bindResolver(resolver, plugins) {
2106
2118
  const {
2107
2119
  getContext: getContext2,
2108
2120
  listItems,
2121
+ validate,
2109
2122
  tryResolveWithoutPrompt,
2110
2123
  tryResolveFromSearch
2111
2124
  } = resolver;
@@ -2119,6 +2132,9 @@ function bindResolver(resolver, plugins) {
2119
2132
  };
2120
2133
  if (getContext2)
2121
2134
  bound.getContext = ({ input }) => getContext2({ imports, input });
2135
+ if (validate) {
2136
+ bound.validate = ({ value, input, context }) => validate({ imports, value, input, context });
2137
+ }
2122
2138
  if (tryResolveWithoutPrompt) {
2123
2139
  bound.tryResolveWithoutPrompt = ({ input }) => tryResolveWithoutPrompt({ imports, input });
2124
2140
  }
@@ -2798,14 +2814,14 @@ function coerce(leaf, raw) {
2798
2814
  }
2799
2815
  async function validationError(leaf, value, state) {
2800
2816
  if (leaf.resolver?.type !== "dynamic") return null;
2817
+ const validate = leaf.resolver.validate;
2818
+ if (!validate) return null;
2801
2819
  const context = await resolveContext(leaf, state.resolved);
2802
- const config = leaf.resolver.prompt?.({
2803
- items: state.listing?.items ?? [],
2820
+ const verdict = await validate({
2821
+ value,
2804
2822
  input: mergeInput(state.resolved, leaf.extraInput),
2805
2823
  context
2806
2824
  });
2807
- if (!config?.validate) return null;
2808
- const verdict = config.validate(value);
2809
2825
  if (verdict === true) return null;
2810
2826
  return typeof verdict === "string" ? verdict : `${leaf.name}: invalid value`;
2811
2827
  }
@@ -2931,20 +2947,36 @@ async function resolveContext(leaf, input) {
2931
2947
  input: mergeInput(input, leaf.extraInput)
2932
2948
  });
2933
2949
  }
2934
- async function fetchListing(leaf, input, opts = {}) {
2950
+ function firstPagePosition(opts = {}) {
2951
+ return {
2952
+ ...opts.search !== void 0 ? { search: opts.search } : {},
2953
+ pageCursor: null,
2954
+ previousCursors: [],
2955
+ generation: opts.generation ?? 0
2956
+ };
2957
+ }
2958
+ async function fetchListing(leaf, input, position, context) {
2959
+ if (leaf.resolver?.type === "dynamic" && leaf.resolver.inputType === "search" && position.search === void 0) {
2960
+ return { position, items: [] };
2961
+ }
2935
2962
  const page = await firstPage(
2936
2963
  leaf.resolver?.type === "dynamic" ? leaf.resolver.listItems({
2937
2964
  input: mergeInput(input, leaf.extraInput),
2938
- context: opts.context,
2939
- search: opts.search,
2940
- cursor: opts.cursor
2965
+ context,
2966
+ search: position.search,
2967
+ cursor: position.pageCursor ?? void 0
2941
2968
  }) : void 0
2942
2969
  );
2943
2970
  return {
2944
- items: [...opts.priorItems ?? [], ...page.data],
2945
- cursor: page.nextCursor,
2946
- search: opts.search,
2947
- exhausted: page.nextCursor == null
2971
+ position,
2972
+ items: page.data,
2973
+ ...page.nextCursor != null ? { nextCursor: page.nextCursor } : {}
2974
+ };
2975
+ }
2976
+ function toPagination(page) {
2977
+ return {
2978
+ position: page.position,
2979
+ ...page.nextCursor !== void 0 ? { nextCursor: page.nextCursor } : {}
2948
2980
  };
2949
2981
  }
2950
2982
 
@@ -2966,23 +2998,32 @@ var AFFORDANCE = {
2966
2998
  description: "Filter the options by a search term",
2967
2999
  supply: "term"
2968
3000
  },
2969
- more: { action: "more", description: "Load more options" },
3001
+ nextPage: {
3002
+ action: "next_page",
3003
+ description: "Fetch the next page of options"
3004
+ },
3005
+ previousPage: {
3006
+ action: "previous_page",
3007
+ description: "Return to the previous page of options"
3008
+ },
2970
3009
  skip: { action: "skip", description: "Omit this optional parameter" },
2971
3010
  add: { action: "add", description: "Add another item" },
2972
3011
  done: { action: "done", description: "Finish the list" },
2973
3012
  retry: { action: "retry", description: "Retry loading the options" },
2974
3013
  cancel: { action: "cancel", description: "Cancel resolution" }
2975
3014
  };
2976
- function selectActions(leaf, listing, multiple) {
3015
+ function selectActions(leaf, page, multiple) {
2977
3016
  const searchMode = leaf.resolver?.type === "dynamic" && leaf.resolver.inputType === "search";
2978
- if (searchMode && listing.search === void 0 && listing.items.length === 0) {
3017
+ if (searchMode && page.position.search === void 0 && page.items.length === 0) {
2979
3018
  const actions2 = multiple ? [AFFORDANCE.search] : [AFFORDANCE.search, AFFORDANCE.custom];
2980
3019
  if (!leaf.required) actions2.push(AFFORDANCE.skip);
2981
3020
  return actions2;
2982
3021
  }
2983
3022
  const actions = multiple ? [AFFORDANCE.choose] : [AFFORDANCE.choose, AFFORDANCE.custom];
2984
3023
  if (searchMode) actions.push(AFFORDANCE.search);
2985
- if (listing.cursor) actions.push(AFFORDANCE.more);
3024
+ if (page.nextCursor) actions.push(AFFORDANCE.nextPage);
3025
+ if (page.position.previousCursors.length > 0)
3026
+ actions.push(AFFORDANCE.previousPage);
2986
3027
  if (!leaf.required) actions.push(AFFORDANCE.skip);
2987
3028
  return actions;
2988
3029
  }
@@ -2990,16 +3031,17 @@ function labeledMessage(leaf) {
2990
3031
  if (!leaf.label) return void 0;
2991
3032
  return `${leaf.label} (${leaf.required ? "required" : "optional"}):`;
2992
3033
  }
2993
- function selectQuestion(leaf, input, listing, context) {
3034
+ function selectQuestion(leaf, path, input, page, context) {
2994
3035
  const resolver = leaf.resolver?.type === "dynamic" ? leaf.resolver : void 0;
2995
3036
  const config = resolver?.prompt?.({
2996
- items: listing.items,
3037
+ items: page.items,
2997
3038
  input: mergeInput(input, leaf.extraInput),
2998
3039
  context
2999
3040
  });
3000
3041
  const multiple = config?.type === "checkbox";
3001
3042
  return {
3002
3043
  type: "select",
3044
+ path,
3003
3045
  // A labeled field's title beats the resolver's message: per-field
3004
3046
  // resolvers are shared across fields (one choices-fetcher for every
3005
3047
  // field), so only the leaf knows which field is being asked.
@@ -3007,9 +3049,13 @@ function selectQuestion(leaf, input, listing, context) {
3007
3049
  choices: (config?.choices ?? []).map(toChoice),
3008
3050
  ...multiple ? { multiple: true } : {},
3009
3051
  ...config?.notes?.length ? { notes: config.notes } : {},
3010
- ...listing.search !== void 0 ? { search: listing.search } : {},
3052
+ ...page.position.search !== void 0 ? { search: page.position.search } : {},
3011
3053
  ...resolver?.placeholder ? { placeholder: resolver.placeholder } : {},
3012
- actions: selectActions(leaf, listing, multiple)
3054
+ page: {
3055
+ generation: page.position.generation,
3056
+ index: page.position.previousCursors.length
3057
+ },
3058
+ actions: selectActions(leaf, page, multiple)
3013
3059
  };
3014
3060
  }
3015
3061
  function toControllerError(error) {
@@ -3031,6 +3077,7 @@ function failedResult(state, name, error) {
3031
3077
  error: toControllerError(error),
3032
3078
  question: {
3033
3079
  type: "select",
3080
+ path: state.current ?? [],
3034
3081
  message: `Could not load options for ${name}.`,
3035
3082
  choices: [],
3036
3083
  actions: [AFFORDANCE.retry, AFFORDANCE.cancel]
@@ -3038,27 +3085,15 @@ function failedResult(state, name, error) {
3038
3085
  }
3039
3086
  };
3040
3087
  }
3041
- async function buildQuestion(leaf, input) {
3088
+ async function buildQuestion(leaf, path, input) {
3042
3089
  const optional = !leaf.required;
3043
3090
  const resolver = leaf.resolver;
3044
3091
  if (resolver?.type === "dynamic") {
3045
3092
  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 });
3093
+ const page = await fetchListing(leaf, input, firstPagePosition(), context);
3059
3094
  return {
3060
- question: selectQuestion(leaf, input, listing, context),
3061
- listing
3095
+ question: selectQuestion(leaf, path, input, page, context),
3096
+ pagination: toPagination(page)
3062
3097
  };
3063
3098
  }
3064
3099
  if (leaf.staticChoices) {
@@ -3067,6 +3102,7 @@ async function buildQuestion(leaf, input) {
3067
3102
  return {
3068
3103
  question: {
3069
3104
  type: "select",
3105
+ path,
3070
3106
  message: `Select ${leaf.name}:`,
3071
3107
  choices: leaf.staticChoices,
3072
3108
  actions: actions2
@@ -3080,6 +3116,7 @@ async function buildQuestion(leaf, input) {
3080
3116
  return {
3081
3117
  question: {
3082
3118
  type: "input",
3119
+ path,
3083
3120
  // The optional marker makes Enter-to-pass discoverable on a bare
3084
3121
  // parameter; a labeled field carries its marker via labeledMessage.
3085
3122
  message: labeledMessage(leaf) ?? `Enter ${leaf.name}${optional ? " (optional)" : ""}:`,
@@ -3094,6 +3131,7 @@ function collectionQuestion(t) {
3094
3131
  if (t.count >= t.min) actions.push(AFFORDANCE.done);
3095
3132
  return {
3096
3133
  type: "collection",
3134
+ path: t.path,
3097
3135
  message: `Add ${t.path[t.path.length - 1]}[${t.count}]?`,
3098
3136
  container: "array",
3099
3137
  count: t.count,
@@ -3107,6 +3145,7 @@ function collectionQuestion(t) {
3107
3145
  function objectGateQuestion(path) {
3108
3146
  return {
3109
3147
  type: "collection",
3148
+ path,
3110
3149
  message: `Add ${path[path.length - 1]}?`,
3111
3150
  container: "object",
3112
3151
  actions: [
@@ -3118,9 +3157,10 @@ function objectGateQuestion(path) {
3118
3157
  ]
3119
3158
  };
3120
3159
  }
3121
- function optionalsGateQuestion(pending) {
3160
+ function optionalsGateQuestion(path, pending) {
3122
3161
  return {
3123
3162
  type: "collection",
3163
+ path,
3124
3164
  // The prompt and its context ride separately so a host renders the info
3125
3165
  // line above the confirm without composing any text of its own.
3126
3166
  message: "Would you like to configure optional fields?",
@@ -3270,8 +3310,12 @@ async function askLeaf(state, path, leaf, opts = {}) {
3270
3310
  state.current = path;
3271
3311
  delete state.gate;
3272
3312
  try {
3273
- const { question, listing } = await buildQuestion(leaf, state.resolved);
3274
- state.listing = listing;
3313
+ const { question, pagination } = await buildQuestion(
3314
+ leaf,
3315
+ path,
3316
+ state.resolved
3317
+ );
3318
+ state.pagination = pagination;
3275
3319
  return {
3276
3320
  state,
3277
3321
  result: {
@@ -3281,7 +3325,7 @@ async function askLeaf(state, path, leaf, opts = {}) {
3281
3325
  }
3282
3326
  };
3283
3327
  } catch (error) {
3284
- state.listing = { items: [], exhausted: false };
3328
+ state.pagination = failedPagination(void 0, firstPagePosition());
3285
3329
  return failedResult(state, leaf.name, error);
3286
3330
  }
3287
3331
  }
@@ -3291,13 +3335,13 @@ async function advance(ctx, state) {
3291
3335
  if (!target) {
3292
3336
  delete state.current;
3293
3337
  delete state.gate;
3294
- delete state.listing;
3338
+ delete state.pagination;
3295
3339
  return { state, result: finalize(ctx, state.resolved) };
3296
3340
  }
3297
3341
  if (target.kind === "array") {
3298
3342
  state.current = target.path;
3299
3343
  state.gate = "array";
3300
- delete state.listing;
3344
+ delete state.pagination;
3301
3345
  return {
3302
3346
  state,
3303
3347
  result: { status: "ask", question: collectionQuestion(target) }
@@ -3306,7 +3350,7 @@ async function advance(ctx, state) {
3306
3350
  if (target.kind === "object") {
3307
3351
  state.current = target.path;
3308
3352
  state.gate = "entry";
3309
- delete state.listing;
3353
+ delete state.pagination;
3310
3354
  return {
3311
3355
  state,
3312
3356
  result: { status: "ask", question: objectGateQuestion(target.path) }
@@ -3315,12 +3359,12 @@ async function advance(ctx, state) {
3315
3359
  if (target.kind === "optionals") {
3316
3360
  state.current = target.path;
3317
3361
  state.gate = "optionals";
3318
- delete state.listing;
3362
+ delete state.pagination;
3319
3363
  return {
3320
3364
  state,
3321
3365
  result: {
3322
3366
  status: "ask",
3323
- question: optionalsGateQuestion(target.pending)
3367
+ question: optionalsGateQuestion(target.path, target.pending)
3324
3368
  }
3325
3369
  };
3326
3370
  }
@@ -3371,13 +3415,13 @@ async function step(ctx, prior, action) {
3371
3415
  if (action.type === "cancel") {
3372
3416
  delete state.current;
3373
3417
  delete state.gate;
3374
- delete state.listing;
3418
+ delete state.pagination;
3375
3419
  return { state, result: { status: "cancelled" } };
3376
3420
  }
3377
3421
  const path = state.current;
3378
3422
  if (!path) throw new Error("step called with no outstanding question");
3379
3423
  const leaf = await leafAt(ctx, path, state.resolved);
3380
- if (leaf && (action.type === "search" || action.type === "more" || action.type === "retry")) {
3424
+ if (leaf && (action.type === "search" || action.type === "next_page" || action.type === "previous_page" || action.type === "retry")) {
3381
3425
  return refine(ctx, state, leaf, path, action);
3382
3426
  }
3383
3427
  if (action.type === "add" || action.type === "done") {
@@ -3389,7 +3433,7 @@ async function step(ctx, prior, action) {
3389
3433
  }
3390
3434
  delete state.current;
3391
3435
  delete state.gate;
3392
- delete state.listing;
3436
+ delete state.pagination;
3393
3437
  if (action.type === "done") {
3394
3438
  settle(state, path);
3395
3439
  return advance(ctx, state);
@@ -3418,23 +3462,44 @@ async function step(ctx, prior, action) {
3418
3462
  case "choose":
3419
3463
  case "custom": {
3420
3464
  if (leaf) {
3421
- const error = await validationError(leaf, action.value, state);
3465
+ let error;
3466
+ try {
3467
+ error = await validationError(leaf, action.value, state);
3468
+ } catch (thrown) {
3469
+ return failedResult(state, leaf.name, thrown);
3470
+ }
3422
3471
  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
- };
3472
+ if (state.pagination && leaf.resolver?.type === "dynamic") {
3473
+ try {
3474
+ const context = await resolveContext(leaf, state.resolved);
3475
+ const page = await fetchListing(
3476
+ leaf,
3477
+ state.resolved,
3478
+ state.pagination.position,
3479
+ context
3480
+ );
3481
+ state.pagination = toPagination(page);
3482
+ return {
3483
+ state,
3484
+ result: {
3485
+ status: "ask",
3486
+ question: selectQuestion(
3487
+ leaf,
3488
+ path,
3489
+ state.resolved,
3490
+ page,
3491
+ context
3492
+ ),
3493
+ error
3494
+ }
3495
+ };
3496
+ } catch (fetchError) {
3497
+ state.pagination = failedPagination(
3498
+ state.pagination,
3499
+ state.pagination.position
3500
+ );
3501
+ return failedResult(state, leaf.name, fetchError);
3502
+ }
3438
3503
  }
3439
3504
  return askLeaf(state, path, leaf, { error });
3440
3505
  }
@@ -3453,15 +3518,11 @@ async function step(ctx, prior, action) {
3453
3518
  throw new Error(`action "${action.type}" is not supported here`);
3454
3519
  }
3455
3520
  delete state.current;
3456
- delete state.listing;
3521
+ delete state.pagination;
3457
3522
  return advance(ctx, state);
3458
3523
  }
3459
3524
  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
- };
3525
+ const position = positionAfter(state.pagination, action);
3465
3526
  try {
3466
3527
  if (action.type === "search") {
3467
3528
  const exact = leaf.resolver?.type === "dynamic" ? await leaf.resolver.tryResolveFromSearch?.({
@@ -3471,32 +3532,62 @@ async function refine(ctx, state, leaf, path, action) {
3471
3532
  if (exact) {
3472
3533
  setAtPath(state.resolved, path, coerce(leaf, exact.resolvedValue));
3473
3534
  delete state.current;
3474
- delete state.listing;
3535
+ delete state.pagination;
3475
3536
  return advance(ctx, state);
3476
3537
  }
3477
3538
  }
3478
3539
  const context = await resolveContext(leaf, state.resolved);
3479
- state.listing = await fetchListing(leaf, state.resolved, {
3480
- ...attempt,
3481
- context
3482
- });
3540
+ const page = await fetchListing(leaf, state.resolved, position, context);
3541
+ state.pagination = toPagination(page);
3483
3542
  return {
3484
3543
  state,
3485
3544
  result: {
3486
3545
  status: "ask",
3487
- question: selectQuestion(leaf, state.resolved, state.listing, context)
3546
+ question: selectQuestion(leaf, path, state.resolved, page, context)
3488
3547
  }
3489
3548
  };
3490
3549
  } catch (error) {
3491
- state.listing = {
3492
- items: attempt.priorItems,
3493
- search: attempt.search,
3494
- cursor: attempt.cursor,
3495
- exhausted: false
3496
- };
3550
+ state.pagination = failedPagination(state.pagination, position);
3497
3551
  return failedResult(state, leaf.name, error);
3498
3552
  }
3499
3553
  }
3554
+ function failedPagination(pagination, retryPosition) {
3555
+ return {
3556
+ position: pagination?.position ?? firstPagePosition(),
3557
+ ...pagination?.nextCursor !== void 0 ? { nextCursor: pagination.nextCursor } : {},
3558
+ retryPosition
3559
+ };
3560
+ }
3561
+ function positionAfter(pagination, action) {
3562
+ const current = pagination?.position ?? firstPagePosition();
3563
+ switch (action.type) {
3564
+ case "search":
3565
+ return firstPagePosition({
3566
+ search: action.term,
3567
+ generation: current.generation + 1
3568
+ });
3569
+ case "next_page": {
3570
+ if (pagination?.nextCursor == null) return current;
3571
+ return {
3572
+ ...current.search !== void 0 ? { search: current.search } : {},
3573
+ pageCursor: pagination.nextCursor,
3574
+ previousCursors: [...current.previousCursors, current.pageCursor],
3575
+ generation: current.generation
3576
+ };
3577
+ }
3578
+ case "previous_page": {
3579
+ if (current.previousCursors.length === 0) return current;
3580
+ return {
3581
+ ...current.search !== void 0 ? { search: current.search } : {},
3582
+ pageCursor: current.previousCursors[current.previousCursors.length - 1],
3583
+ previousCursors: current.previousCursors.slice(0, -1),
3584
+ generation: current.generation
3585
+ };
3586
+ }
3587
+ case "retry":
3588
+ return pagination?.retryPosition ?? current;
3589
+ }
3590
+ }
3500
3591
 
3501
3592
  // src/model/resolution/controller.ts
3502
3593
  function toJsonSchema(schema) {
@@ -3679,6 +3770,7 @@ function openEnum(values, description) {
3679
3770
  CoreSignal,
3680
3771
  addPlugin,
3681
3772
  composePlugins,
3773
+ concatLists,
3682
3774
  concatPaginated,
3683
3775
  coreOptionsPluginRef,
3684
3776
  createAsyncContext,