Advanced

Lifecycle Methods

Understanding plugin lifecycle hooks in Universal Data Layer

Plugin Lifecycle

UDL plugins have access to lifecycle hooks that execute at specific points during initialization and runtime.

Execution Flow

1. Load application config (udl.config.ts)
   ↓
2. Execute app's onLoad hook (if defined)
   ↓
3. For each plugin (in order):
   a. Resolve plugin path
   b. Load plugin's udl.config.ts
   c. Execute plugin's onLoad hook
   d. Register referenceResolver (if exported)
   e. Register entityKeyConfig (if exported)
   ↓
4. For each source plugin:
   - Execute sourceNodes hook
   - Process created nodes
   ↓
5. Build GraphQL schema
   ↓
6. Start GraphQL server

Available Hooks

onLoad

Called when the plugin is loaded. Use for validation and initialization:

udl.config.ts
export function onLoad({
  options,
  config,
}: {
  options?: MyPluginOptions;
  config: UDLConfig;
}) {
  // Validate required options
  if (!options?.apiKey) {
    throw new Error('apiKey is required');
  }

  // Access application config
  console.log(`Server will run on port ${config.port}`);
}

Context Properties

PropertyTypeDescription
optionsobject | undefinedPlugin-specific options from configuration
configUDLConfigThe full application configuration

sourceNodes

Called for source plugins to fetch and create nodes:

udl.config.ts
import type { SourceNodesContext } from 'universal-data-layer';

export async function sourceNodes({
  actions,
  createNodeId,
  createContentDigest,
  options,
  cacheDir,
}: SourceNodesContext<MyPluginOptions>): Promise<void> {
  const items = await fetchData(options);

  for (const item of items) {
    await actions.createNode({
      id: createNodeId(`Item-${item.id}`),
      internal: {
        type: 'Item',
        contentDigest: createContentDigest(item),
      },
      ...item,
    });
  }
}

Context Properties

PropertyTypeDescription
actionsNodeActionsFunctions to create/delete nodes
createNodeId(id: string) => stringCreates unique node IDs
createContentDigest(data: unknown) => stringCreates content hashes
optionsT | undefinedPlugin options (typed via generic)
cacheDirstringPath to plugin's cache directory

Plugin Exports

referenceResolver

Tells UDL how to resolve linked content:

udl.config.ts
import type { ReferenceResolverConfig } from 'universal-data-layer';

export const referenceResolver: ReferenceResolverConfig = {
  id: 'my-plugin',
  markerField: '_myRef',
  lookupField: 'externalId',
  isReference: (value): value is MyReference => {
    return typeof value === 'object' && '_myRef' in value;
  },
  getLookupValue: (ref) => ref.refId,
  getPossibleTypes: (ref) => ref.possibleTypes ?? [],
  priority: 10,
};
PropertyDescription
idUnique identifier for this resolver
markerFieldField that marks an object as a reference
lookupFieldField to use for looking up the referenced node
isReferenceType guard to identify reference objects
getLookupValueExtracts the lookup value from a reference
getPossibleTypesReturns possible GraphQL types for the reference
priorityResolution priority (higher = checked first)

entityKeyConfig

Defines how nodes are uniquely identified:

udl.config.ts
import type { EntityKeyConfig } from 'universal-data-layer';

export const entityKeyConfig: EntityKeyConfig = {
  idField: 'externalId',
  priority: 10,
};
PropertyDescription
idFieldField name used as the unique identifier
priorityPriority for ID resolution (higher = preferred)

Async Support

All lifecycle hooks support async operations:

udl.config.ts
export async function onLoad({ options }: { options?: MyPluginOptions }) {
  await validateCredentials(options);
  await warmupCache(options);
  console.log('Plugin ready');
}

export async function sourceNodes(context: SourceNodesContext) {
  const items = await fetchAllItems(context.options);

  await Promise.all(
    items.map((item) => context.actions.createNode(transformItem(item)))
  );
}

UDL waits for each promise to resolve before continuing.

Error Handling

Throwing Errors

Throw errors to halt plugin loading:

udl.config.ts
export function onLoad({ options }: { options?: MyPluginOptions }) {
  if (!options?.apiKey) {
    throw new Error('API key is required');
  }

  if (!isValidApiKey(options.apiKey)) {
    throw new Error('Invalid API key format');
  }
}

Graceful Degradation

For non-critical failures, log warnings:

udl.config.ts
export async function sourceNodes(context: SourceNodesContext) {
  const items = await fetchItems(context.options);

  for (const item of items) {
    try {
      await context.actions.createNode(transformItem(item));
    } catch (error) {
      console.warn(`Warning: Failed to process item ${item.id}:`, error);
      // Continue with other items
    }
  }
}

Custom Error Classes

Create specific error types for better debugging:

errors.ts
export class ConfigError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'ConfigError';
  }
}

export class ApiError extends Error {
  constructor(message: string, public statusCode?: number) {
    super(message);
    this.name = 'ApiError';
  }
}

export class SyncError extends Error {
  constructor(message: string, public isRetryable: boolean) {
    super(message);
    this.name = 'SyncError';
  }
}

Best Practices

1. Validate Early

Check required options in onLoad before sourceNodes runs:

export function onLoad({ options }: { options?: MyPluginOptions }) {
  const required = ['apiKey', 'spaceId'] as const;

  for (const key of required) {
    if (!options?.[key]) {
      throw new Error(`Missing required option: ${key}`);
    }
  }
}

2. Use Parallel Operations

Batch async operations for better performance:

export async function sourceNodes({ actions, options }: SourceNodesContext) {
  const [products, categories, tags] = await Promise.all([
    fetchProducts(options),
    fetchCategories(options),
    fetchTags(options),
  ]);

  // Process all items
}

3. Provide Detailed Logging

Use consistent log prefixes:

const LOG_PREFIX = '[my-plugin]';

export function onLoad({ options }: { options?: MyPluginOptions }) {
  console.log(`${LOG_PREFIX} Initializing...`);
}

export async function sourceNodes(context: SourceNodesContext) {
  console.log(`${LOG_PREFIX} Fetching data...`);
  // ...
  console.log(`${LOG_PREFIX} Created ${count} nodes`);
}

4. Handle Incremental Updates

Use the cache directory to store sync tokens:

import { readFileSync, writeFileSync, existsSync } from 'fs';
import { join } from 'path';

export async function sourceNodes({ cacheDir, options }: SourceNodesContext) {
  const tokenPath = join(cacheDir, 'sync-token.json');

  // Load previous sync token
  let syncToken: string | undefined;
  if (existsSync(tokenPath)) {
    syncToken = JSON.parse(readFileSync(tokenPath, 'utf-8')).token;
  }

  // Perform sync
  const result = await syncData(options, syncToken);

  // Store new sync token
  writeFileSync(tokenPath, JSON.stringify({ token: result.nextSyncToken }));
}

Next Steps