v3.0.0

FetchTailV3.make

Async generator that streams all records of a REST API version 3 tail method through the server's native keyset cursor.

Overview

FetchTailV3.make() walks a restApi:v3 tail method through the server's native keyset cursor and yields each page as it arrives, so a dataset larger than memory can be processed a page at a time.

It is the streaming counterpart of CallTail; everything about the cursor, the filter shapes and the errors is the same, and that page documents it in full. What follows is what differs.

Check that the method exists first. A tail endpoint is separate from its list counterpart and few modules publish one — see how to ask the portal. For an ordinary list method use FetchList.
for await (const page of $b24.actions.v3.fetchTail.make<{ id: number }>({
  method: 'main.eventlog.tail',
  params: { select: ['id', 'auditTypeId'] },
  cursorField: 'id',
  customKeyForResult: 'items',
  limit: 100
})) {
  // `page` is one server page, already parsed
  console.log(`Processing ${page.length} items`)
}

When to Use FetchTailV3.make()

  1. Large exports and migrations — rows are handed over and can be released, instead of accumulating.
  2. Early exit — break out of the loop and no further request is made.
  3. Progress that means something — you see each page as it lands.

Use CallTail instead when the whole result fits in memory and one array is simpler to work with.

Method Signature

make<T = unknown>(
  options: ActionFetchTailV3
): AsyncGenerator<T[]>

The options are those of CallTail: method, params, cursorField, order, initialValue, customKeyForResult, requestId, limit, maxPages and signal.

There is no progress callback here, and none is needed — the loop body runs once per page, which is the same signal a callback would carry.

Two rules from that page apply here unchanged and are easy to miss coming from the list walkers: the cursor field must not appear in filter (the server refuses it), and filter may be the array form or a bare FilterV3 logic group — the list walkers accept only the array, because they have to extend it. See Choosing a cursor field for what the field itself has to be.

Return Value

AsyncGenerator<T[]> — yields one array per server page. Nothing happens until you consume it: the first request is made on the first iteration, so a generator that is created and never iterated sends no traffic at all. That is also where every guard fires — wrapping only the make(...) call in a try catches nothing.

An empty result yields no page at all, so the loop body never runs. A wrong customKeyForResult looks identical, because a key the walker cannot find reads as "no data" — check the key before concluding the portal had nothing.

Error Handling

Everything here throws, which is the difference from callTail worth writing code around.

CodeWhen
JSSDK_ACTION_V3_TAIL_FILTER_INVALIDfilter was the restApi:v2 object dialect. Raised on the first iteration, before any request.
JSSDK_CORE_B24_FETCH_TAIL_DESC_REQUIRES_INITIAL_VALUEorder: 'DESC' without initialValue. Note the FETCH_TAIL segment: callTail raises the CALL_TAIL spelling, so match the exact string.
JSSDK_CORE_B24_FETCH_TAIL_METHOD_API_V3The portal returned a soft error mid-walk. This is the one you will see most often, and it is the real asymmetry: callTail folds the same failure into its Result instead.
JSSDK_ACTION_CURSOR_STALLEDThe cursor came back equal to the one just sent. Usually a cursorField that is not the field the server pages by, or a method with no tail endpoint at all.
JSSDK_ACTION_CURSOR_WENT_BACKWARDSThe cursor moved into a value already passed. With DESC, check initialValue is the newest value.
JSSDK_ACTION_MAX_PAGES_EXCEEDEDThe page ceiling was reached.
JSSDK_ACTION_INVALID_MAX_PAGESmaxPages was not a positive integer. Raised on the first iteration, before any request.
JSSDK_ACTION_ABORTEDThe signal fired.

Where this really differs from CallTail

Three of these reach you differently:

  • JSSDK_ACTION_MAX_PAGES_EXCEEDED and JSSDK_ACTION_ABORTED are bounds you set, so callTail hands back the rows it collected with the error attached. A generator cannot — it has already yielded every page it read;
  • a soft portal error is reported by callTail on the Result and here as JSSDK_CORE_B24_FETCH_TAIL_METHOD_API_V3.

The rest throw from both walkers: await callTail.make(...) without a try rejects on a stalled cursor exactly as the loop below does.

callTail resolves a Result and attaches a bound-related error to it, so the rows it did read survive. A generator cannot do that — it has already handed over every page it read — so it throws instead:

try {
  for await (const page of $b24.actions.v3.fetchTail.make({
    method: 'main.eventlog.tail',
    cursorField: 'id',
    customKeyForResult: 'items'
  })) {
    console.log(`Processing ${page.length} items`)
  }
}
catch (error) {
  // The pages already yielded have been processed. If that is not safe for your
  // consumer, undo them here — see the note below.
  throw error
}
A page that was yielded was already delivered. If the walk stops with JSSDK_ACTION_CURSOR_STALLED or JSSDK_ACTION_CURSOR_WENT_BACKWARDS, the pages your loop already processed may contain duplicates — the server was repeating itself. A consumer that persisted them has to undo that. This is the opposite of JSSDK_ACTION_MAX_PAGES_EXCEEDED and JSSDK_ACTION_ABORTED, where what was read is correct and merely incomplete.

What else differs from CallTail

Stopping early

Leaving the loop stops the walk; no further page is requested.

let seen = 0

for await (const page of $b24.actions.v3.fetchTail.make<{ id: number }>({
  method: 'main.eventlog.tail',
  cursorField: 'id',
  customKeyForResult: 'items'
})) {
  seen += page.length
  if (seen >= 500) {
    break
  }
}

For a bound the SDK enforces rather than one you check yourself, pass maxPages or a signal.

Examples

Streaming an export, oldest first

import { SdkError } from '@bitrix24/b24jssdk'

let pages = 0
let rows = 0

try {
  for await (const page of $b24.actions.v3.fetchTail.make<{ id: number }>({
    method: 'main.eventlog.tail',
    params: { select: ['id', 'auditTypeId'] },
    cursorField: 'id',
    customKeyForResult: 'items',
    limit: 100
  })) {
    rows += page.length
    console.log(`page ${++pages}, ${rows} rows`)
  }
}
catch (error) {
  if (error instanceof SdkError) {
    console.error(`walk stopped: ${error.code}`)
  }
  throw error
}

Newest first

A descending walk needs an explicit starting point, because the server pages by field < value:

for await (const page of $b24.actions.v3.fetchTail.make<{ id: number }>({
  method: 'main.eventlog.tail',
  cursorField: 'id',
  order: 'DESC',
  initialValue: Number.MAX_SAFE_INTEGER,
  customKeyForResult: 'items'
})) {
  console.log(`Processing ${page.length} items`)
}

Without initialValue the walk raises JSSDK_CORE_B24_FETCH_TAIL_DESC_REQUIRES_INITIAL_VALUE rather than walking away empty. Number.MAX_SAFE_INTEGER suits a numeric field; for a datetime or any other string, see what to pass instead. It fires on the first iteration, not when the generator is created — nothing runs until you consume it.

Alternatives and Recommendations