How to explicitly "discard" and "recreate" Provider objects for transaction management?

Hi everyone,

I am currently implementing EPG integration for Amazon Vega OS and have a question regarding transaction management for Providers.

According to the Kepler EPG Provider API Guidelines:

  1. The ChannelLineupProvider, ProgramLineupProvider, and LiveEventProvider interfaces are transactional.
     a. The commit API must be called at the end to complete the transaction. After calling commit(), the object cannot be reused. A new object must be created if necessary.

     b.Destroying the ChannelLineupProvider, ProgramLineupProvider, and LiveEventProvider objects without calling a commit aborts the transaction.

The Issue: I would like to know the correct way to “discard” these objects to intentionally abort a transaction (e.g., when an add() operation fails).

Looking at the type definitions, these provider objects appear to be singletons. Since I cannot find any explicit close(), destroy(), or abort() methods within the interface, I do not know how to “discard” the object in order to trigger the abort logic mentioned in the documentation.

My Questions:

  1. How do I “discard” the object? What is the specific procedure or API call required to “discard” the provider object and ensure the transaction is aborted?
  2. How should the object be recreated? The guide states that “a new object must be created” after a commit or discard. Could you clarify the specific steps or methods required to regenerate a new instance for the next transaction?
  3. Is there an explicit Abort API? Is there a dedicated abort() or rollback() method available that is not mentioned in the summary guidelines?

I want to ensure that failed updates do not leave partial data or lock the provider state. Any clarification on the lifecycle management of these objects would be greatly appreciated.

Best regards,

Tatsuki

Hello @ishijima_tatsuki_1

Here are the answers to your questions about EPG provider transaction management:

## 1. How to “Discard” the Object {}
In JavaScript/TypeScript, “destroying” an object means letting it go out of scope {} or explicitly setting it to null/undefined so it becomes eligible for garbage collection. There is no explicit destroy() or abort() method.

let channelProvider = await getChannelLineupProvider();
try
{ await channelProvider.add(channels); await channelProvider.commit(); // Success path }
catch (error)
{ // Abort by discarding the object - don't call commit() channelProvider = null; // Let it be garbage collected // Transaction is automatically aborted throw error; }

## 2. How to Recreate the Object {}
After a commit() or abort (discard), get a fresh provider instance from the factory function:

// After commit or abort, create new instance
channelProvider = await getChannelLineupProvider();

The provider factory functions (like getChannelLineupProvider(), getProgramLineupProvider(), etc.) return new transactional instances.

## 3. Is There an Explicit Abort API? {}
No. The documentation explicitly states that abortion happens by “destroying” the object without calling commit(). This is intentional - simply don’t call commit() and let the object be garbage collected.

## Practical Pattern {}

async function syncChannels(channels) {
let provider = await getChannelLineupProvider();
try {
const failures = await provider.add(channels);
if (failures.length > 0) {
// Log failures to backend
await logFailuresToBackend(failures);
// Decision point: abort or partial commit
provider = null; // Abort transaction
throw new Error(`Failed to add ${failures.length} channels`);
}
await provider.commit();
} catch (error)
{ provider = null; // Ensure abort on any error throw error; }
}

The key insight: these providers use implicit transaction management through object lifecycle rather than explicit abort methods. Not calling commit() before the object is garbage collected = transaction abort.

Destroying the ChannelLineupProvider, ProgramLineupProvider, and LiveEventProvider objects without calling a commit aborts the transaction.-- Destroying the objects here means letting it go out of scope or setting them to null explicitly. There is not abort api but throwing an error and intentionally not calling commit for failed updates will abort the transaction.

Q: I want to ensure that failed updates do not leave partial data or lock the provider state. Any clarification on the lifecycle management of these objects would be greatly appreciated.
You can catch all failed updates and choose to continue adding remaining data or abort the transaction.

Here is an example of LiveEventProvider provider for the same

const live_events_page_2 = getLiveEventData(11, 20); const addLiveEventFailures_page_2 = await LiveEventProvider.add(live_events_page_2,); if (addLiveEventFailures_page_2.length > 0) { // TODO: You should catch all the failed live events information and push it to your backend to quickly resolve the issues with the live event data. console.error(`EpgSync - there are ${addLiveEventFailures_page_2.length} live events from page 2 which failed to be added`,); processAddLiveEventFailures(addLiveEventFailures_page_2); // TODO: You can choose to continue adding the remaining live events or abort the live event ingestion process. Calling throw Error will prevent commit from happening and discard the entire transaction, if you choose not to throw error then you can commit only successful updates. throw Error(// 'Abort the live event ingestion process due to invalid live event data in page 2',// ); } // Step 3: Commit the Live Event Lineup using LiveEventProvider.commit const total_live_event_failures = addLiveEventFailures_page_1.length + addLiveEventFailures_page_2.length; console.info( `EpgSync - total number of errored live events ${total_live_event_failures}`, ); // If any live events failed to add, and you did not abort the process when receiving those failure errors, // then you can update the latestVersion for the successfully added live events and send the information about // the failed live events back to your backend so they can be fixed before the next sync. await LiveEventProvider.commit(latestVersion);

Warm regards,
Ivy

Hi , @Ivy_Mahajan

Thank you for your response. However, there are significant discrepancies between your explanation and the actual implementation of the @amazon-devices/kepler-epg-provider (v1.9.79) package.

  1. getChannelLineupProvider() does not exist You suggested using getChannelLineupProvider() to get a fresh instance, but this function is not defined anywhere in the @amazon-devices/kepler-epg-providerv1.9.79.

  2. Looking at ChannelLineupProvider.js, the ChannelLineupProvider class is not exported, and no factory function is provided.

ChannelLineupProvider.js

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const AmazonNativeChannelProvider_1 = __importDefault(require("./AmazonNativeChannelProvider"));
const StatusCodes_1 = require("./types/StatusCodes");
const Error_1 = require("./types/Error");
class ChannelLineupProvider {
    hasInternalError = false;
    hasInvalidArgumentError = false;
    hasStorageLimitError = false;
    /**
     * {@inheritDoc}
     */
    async add(channels) {
        if (this.hasInternalError) {
            throw new Error_1.IllegalStateError('Add channels called after an InternalError was thrown already by this object.');
        }
        if (this.hasInvalidArgumentError) {
            throw new Error_1.IllegalStateError('Add channels called after an InvalidArgumentError was thrown already by this object.');
        }
        if (this.hasStorageLimitError) {
            throw new Error_1.IllegalStateError('Add channels called after a StorageLimitError was thrown already by this object.');
        }
        const status = (await AmazonNativeChannelProvider_1.default.add(channels));
        if ((0, StatusCodes_1.isInternalError)(status.statusCode)) {
            this.hasInternalError = true;
        }
        else if ((0, StatusCodes_1.isInvalidArgumentError)(status.statusCode)) {
            this.hasInvalidArgumentError = true;
        }
        else if ((0, StatusCodes_1.isStorageLimitError)(status.statusCode)) {
            this.hasStorageLimitError = true;
        }
        (0, StatusCodes_1.throwIfError)(status.statusCode, status.errorMessage);
    }
    /**
     * {@inheritDoc}
     */
    async commit(version) {
        if (this.hasInternalError) {
            throw new Error_1.IllegalStateError('Commit channels called after an InternalError was thrown already by this object.');
        }
        if (this.hasInvalidArgumentError) {
            throw new Error_1.IllegalStateError('Add channels called after an InvalidArgumentError was thrown already by this object.');
        }
        const status = (await AmazonNativeChannelProvider_1.default.commit(version));
        if ((0, StatusCodes_1.isInternalError)(status.statusCode)) {
            this.hasInternalError = true;
        }
        else if ((0, StatusCodes_1.isInvalidArgumentError)(status.statusCode)) {
            this.hasInvalidArgumentError = true;
        }
        (0, StatusCodes_1.throwIfError)(status.statusCode, status.errorMessage);
    }
}
exports.default = new ChannelLineupProvider();
  1. The ChannelLineupProvider.d.ts file explicitly instructs developers to instantiate a new object, which is physically impossible given the JS implementation above.

ChannelLineupProvider.d.ts

import { IChannelInfo } from './types/Channel';
/**
 * The data provider object used for replacing the list of channels in the EPG.
 *
 * @details This provider is used to set/replace all of the channels for the EPG to an entirely new set of channels.
 * The new set of channels can be created by calling the `add()` function multiple times with different lists of channels,
 * and then when the `commit()` function is finally called, all previous channels in the EPG will be replaced with the entire
 * set of new channels. If the provider object is destroyed before calling `commit()`, all changes that were queued up will be lost.
 * Please note that this interface is not thread-safe. Do not create multiple instances of this object for use in different threads
 * to avoid non-deterministic behavior.
 * Please discard this object and create a new instance if this object has thrown an error in one of its methods.
 */
export interface IChannelLineupProvider {
    /**
     * Add a list of channels to the commit queue.
     *
     * @details This function can be called one or more times to add channel data for the EPG.
     * Inserted data will only be committed when the `commit()` function is called.
     *
     * @param {IChannelInfo[]} channels An array of channels to be inserted.
     *
     * @throws IllegalStateError if this function is called after the same `IChannelLineupProvider` has already
     * thrown an error previously in one of its methods.
     * @throws InvalidArgumentError if any of the provided channels is invalid. The error message will include
     * the total number of failed insertions and the reasons for the first 5 failed channels.
     * The error message example can be found in the developer guide.
     * @throws InternalError if this function encounters an unrecoverable error such as data store connection issues, etc.
     * The failed channels and errors causing the failure will be included in the error message.
     * @throws StorageLimitError if this function encounters an error due to ingested data exceeding the storage limit.
     * Consult with your Amazon contact if you encounter this error.
     */
    add(channels: IChannelInfo[]): Promise<void>;
    /**
     * Replaces current channel data with the pending channel data added to the commit queue.
     *
     * @details This function should be called after all data has been added to the commit queue using the
     * `add()` function. When the provider is committed, all preexisting channels will be removed from the data store,
     * and replaced with all of the channels added to this provider object.
     * `EpgLineupInformation.getLastCommittedChannelLineupVersion()` will begin to return this version value
     * once this call succeeds.
     *
     * @param {string} version A string representing the version of the channel lineup that is being committed.
     *
     * @throws `IllegalStateError` If this function is called after the Provider has already
     * thrown an error in one of its methods.
     * @throws `InternalError` If the data cannot be committed to the data store.
     */
    commit(version: string): Promise<void>;
}
declare const _default: IChannelLineupProvider;
export default _default;

My Question

Given that getChannelLineupProvider() is missing and the provider is a locked singleton (the class is not exported), how exactly am I supposed to “recreate” the instance as the documentation requires?

Best regards,

Tatsuki

Hi @ishijima_tatsuki_1

I think the confusion is stemming from the fact the getChannelLineupProvider() is not a actual function but something you will have to implement. All you need to do get channelLineup and call the ChannelLineupProvider.add and ChannelLineupProvider.commit functions. Once the function goes out of scope ChannelLineupProvider will be destroyed automatically.

Let me give you a detailed example for this.

This is same as channel injection sample app we have vega-video-sample/src/livetv/task/EpgSyncTask.ts at main · AmazonAppDev/vega-video-sample · GitHub and vega-video-sample/src/livetv/mock/MockSource.ts at main · AmazonAppDev/vega-video-sample · GitHub

As you can see the below example the ChannelLineupProvider singleton is called within the scope of ingestChannelLineup function → vega-video-sample/src/livetv/task/EpgSyncTask.ts at main · AmazonAppDev/vega-video-sample · GitHub so there is no need for you to even implement a getChannelLineupProvider(). Please take a look at the sample app example and let us know if there are any other questions and if that answers all the queries.

export const getMockedChannelLineup = async (
  start: number,
  pageSize: number,
): Promise<IChannelInfo[]> => {
  const channelsJson = await channelsJsonPromise;
  let channels: IChannelInfo[] = [];
  const availableChannels = channelsJson.length;
  if (start >= availableChannels) {
    // mock no more pages available
    return Promise.resolve(channels);
  }
  try {
    console.log(
      '[ MockSource.ts ] - getMockedChannelLineup - Build Channel info',
    );
    const end = Math.min(availableChannels, start + pageSize);
    for (let i = start; i < end; i++) {
      const channel = buildChannelInfo(channelsJson[i]);
      channels.push(channel);
    }
    return Promise.resolve(channels);
  } catch (error) {
    return Promise.reject(error);
  }
};

Usage for the getMockedChannelLineup above vega-video-sample/src/livetv/task/EpgSyncTask.ts at main · AmazonAppDev/vega-video-sample · GitHub

      const pageSize = 1000;      
      let processedChannels = 0;
      let start = 0;
      let channels = await getMockedChannelLineup(start, pageSize);
      while (channels.length !== 0) {
        await ChannelLineupProvider.add(channels);
        processedChannels += channels.length;
        if (progressCallback) {
          progressCallback(processedChannels / totalChannels);
        }
        start += pageSize;
        channels = await getMockedChannelLineup(start, pageSize);
      }
      await ChannelLineupProvider.commit(latestVersion);
      console.info('EpgSync - channel lineup ingestion completed');
      resolve();
    } catch (error) {
      /*
       * NOTE: You should log these errors. Also consider propagating these errors to your backend so you can work with your
       * Amazon contact to fix these errors. If you encounter an InvalidArgumentError, the error message includes the total
       * number of failed insertions and the reasons for the first 5 failed channels. Please fix the invalid channel data ASAP.
       */
      console.error(
        `EpgSync - Error during channel lineup ingestion: ${error}`,
      );
      reject(error);
    }

Warm regards,
Ivy

Hi, @Ivy_Mahajan

Thank you for your response.

As you explained, I now understand that by using ChannelLineupProvider.add and ChannelLineupProvider.commit within the function scope—similar to the sample code—they will be properly disposed of once the function execution is complete.

I will proceed with the implementation by following the sample and keeping these calls within the function scope.

Thank you for the clear explanation.

Best regards,

Tasuki

1 Like