or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

data-processing-summary.mdindex.mdnetwork-utilities.mdprotocol-message-utilities.mdrequest-management.mdretry-rate-limiting.mdstorage-services.mdtree-blob-utilities.mdurl-compression.md
tile.json

data-processing-summary.mddocs/

Data Processing and Summary Management

Utilities for parsing JSON data from storage and managing summary structures. Provides type-safe data processing and summary format handling.

Capabilities

Data Parsing

Type-safe JSON parsing from storage service blobs with automatic UTF-8 decoding.

/**
 * Reads blob from storage service and JSON parses it into type T
 * @param storage - Storage service with readBlob capability
 * @param id - Blob ID to read and parse
 * @returns Promise resolving to parsed object of type T
 */
function readAndParse<T>(
  storage: Pick<IDocumentStorageService, "readBlob">, 
  id: string
): Promise<T>;

Usage Examples:

import { readAndParse } from "@fluidframework/driver-utils";

// Parse configuration from storage
interface AppConfig {
  version: string;
  features: string[];
  debug: boolean;
}

const config = await readAndParse<AppConfig>(storageService, "config-blob-id");
console.log(`App version: ${config.version}`);

// Parse user preferences
interface UserPreferences {
  theme: "light" | "dark";
  language: string;
  notifications: boolean;
}

const preferences = await readAndParse<UserPreferences>(
  storageService, 
  "user-prefs-blob-id"
);

Summary Management

Functions and interfaces for working with combined app and protocol summary structures.

/**
 * Combined app and protocol summary structure
 * Used for create-new and single-commit summaries
 */
interface CombinedAppAndProtocolSummary extends ISummaryTree {
  tree: {
    ".app": ISummaryTree;
    ".protocol": ISummaryTree;
  };
}

/**
 * Type guard for combined app+protocol summary format
 * @param summary - Summary tree to check
 * @param optionalRootTrees - Additional root tree names to allow
 * @returns true if summary has the combined format
 */
function isCombinedAppAndProtocolSummary(
  summary: ISummaryTree | undefined, 
  ...optionalRootTrees: string[]
): summary is CombinedAppAndProtocolSummary;

/**
 * Extracts and parses document attributes from protocol summary
 * @param protocolSummary - Protocol portion of summary tree
 * @returns Parsed document attributes
 */
function getDocAttributesFromProtocolSummary(
  protocolSummary: ISummaryTree
): IDocumentAttributes;

/**
 * Extracts and parses quorum values from protocol summary
 * @param protocolSummary - Protocol portion of summary tree
 * @returns Array of quorum key-value pairs
 */
function getQuorumValuesFromProtocolSummary(
  protocolSummary: ISummaryTree
): [string, ICommittedProposal][];

Usage Examples:

import { 
  isCombinedAppAndProtocolSummary,
  getDocAttributesFromProtocolSummary,
  getQuorumValuesFromProtocolSummary,
  CombinedAppAndProtocolSummary
} from "@fluidframework/driver-utils";

// Check if summary has combined format
function processSummary(summary: ISummaryTree) {
  if (isCombinedAppAndProtocolSummary(summary)) {
    // Process combined summary
    const appSummary = summary.tree[".app"];
    const protocolSummary = summary.tree[".protocol"];
    
    // Extract document attributes
    const docAttributes = getDocAttributesFromProtocolSummary(protocolSummary);
    console.log(`Document created: ${docAttributes.createdTime}`);
    
    // Extract quorum values
    const quorumValues = getQuorumValuesFromProtocolSummary(protocolSummary);
    console.log(`Found ${quorumValues.length} quorum values`);
    
    return { appSummary, protocolSummary, docAttributes, quorumValues };
  } else {
    // Handle regular summary format
    console.log("Processing regular summary format");
    return { summary };
  }
}

Advanced Usage Patterns

Typed Data Parser

import { readAndParse } from "@fluidframework/driver-utils";

class TypedDataParser {
  constructor(private storage: Pick<IDocumentStorageService, "readBlob">) {}
  
  async parseWithValidation<T>(
    blobId: string,
    validator: (data: any) => data is T,
    errorMessage?: string
  ): Promise<T> {
    try {
      const data = await readAndParse<any>(this.storage, blobId);
      
      if (!validator(data)) {
        throw new Error(errorMessage || `Invalid data format in blob ${blobId}`);
      }
      
      return data;
    } catch (error) {
      if (error instanceof SyntaxError) {
        throw new Error(`Invalid JSON in blob ${blobId}: ${error.message}`);
      }
      throw error;
    }
  }
  
  async parseWithDefault<T>(blobId: string, defaultValue: T): Promise<T> {
    try {
      return await readAndParse<T>(this.storage, blobId);
    } catch (error) {
      console.warn(`Failed to parse blob ${blobId}, using default:`, error.message);
      return defaultValue;
    }
  }
  
  async parseMultiple<T>(blobIds: string[]): Promise<T[]> {
    const promises = blobIds.map(id => readAndParse<T>(this.storage, id));
    return Promise.all(promises);
  }
}

// Usage with validators
function isAppConfig(data: any): data is AppConfig {
  return typeof data === 'object' && 
         typeof data.version === 'string' &&
         Array.isArray(data.features) &&
         typeof data.debug === 'boolean';
}

const parser = new TypedDataParser(storageService);
const config = await parser.parseWithValidation(
  "config-blob", 
  isAppConfig,
  "Invalid application configuration"
);

Summary Analyzer

import { 
  isCombinedAppAndProtocolSummary,
  getDocAttributesFromProtocolSummary,
  getQuorumValuesFromProtocolSummary,
  readAndParse
} from "@fluidframework/driver-utils";

class SummaryAnalyzer {
  constructor(private storage: Pick<IDocumentStorageService, "readBlob">) {}
  
  async analyzeSummary(summary: ISummaryTree): Promise<SummaryAnalysis> {
    const analysis: SummaryAnalysis = {
      type: 'unknown',
      appBlobCount: 0,
      protocolBlobCount: 0,
      totalSize: 0,
      documentAttributes: null,
      quorumSize: 0,
      customTrees: []
    };
    
    if (isCombinedAppAndProtocolSummary(summary)) {
      analysis.type = 'combined';
      
      // Analyze app section
      analysis.appBlobCount = this.countBlobs(summary.tree[".app"]);
      
      // Analyze protocol section
      const protocolSummary = summary.tree[".protocol"];
      analysis.protocolBlobCount = this.countBlobs(protocolSummary);
      
      // Extract protocol information
      try {
        analysis.documentAttributes = getDocAttributesFromProtocolSummary(protocolSummary);
        const quorumValues = getQuorumValuesFromProtocolSummary(protocolSummary);
        analysis.quorumSize = quorumValues.length;
      } catch (error) {
        console.warn("Failed to extract protocol information:", error);
      }
      
      // Check for additional root trees
      for (const [key, tree] of Object.entries(summary.tree)) {
        if (key !== ".app" && key !== ".protocol") {
          analysis.customTrees.push({
            name: key,
            blobCount: this.countBlobs(tree)
          });
        }
      }
    } else {
      analysis.type = 'standard';
      analysis.appBlobCount = this.countBlobs(summary);
    }
    
    // Calculate total size by reading blob sizes
    analysis.totalSize = await this.calculateTotalSize(summary);
    
    return analysis;
  }
  
  private countBlobs(tree: ISummaryTree): number {
    let count = 0;
    
    for (const value of Object.values(tree.tree)) {
      if (value.type === SummaryType.Blob) {
        count++;
      } else if (value.type === SummaryType.Tree) {
        count += this.countBlobs(value);
      }
    }
    
    return count;
  }
  
  private async calculateTotalSize(tree: ISummaryTree): Promise<number> {
    let totalSize = 0;
    
    for (const value of Object.values(tree.tree)) {
      if (value.type === SummaryType.Blob) {
        if (typeof value.content === 'string') {
          totalSize += Buffer.byteLength(value.content, 'utf8');
        } else {
          totalSize += value.content.byteLength;
        }
      } else if (value.type === SummaryType.Tree) {
        totalSize += await this.calculateTotalSize(value);
      }
    }
    
    return totalSize;
  }
}

interface SummaryAnalysis {
  type: 'combined' | 'standard' | 'unknown';
  appBlobCount: number;
  protocolBlobCount: number;
  totalSize: number;
  documentAttributes: IDocumentAttributes | null;
  quorumSize: number;
  customTrees: Array<{
    name: string;
    blobCount: number;
  }>;
}

Summary Builder

import { 
  CombinedAppAndProtocolSummary,
  readAndParse 
} from "@fluidframework/driver-utils";

class SummaryBuilder {
  private appTree: Record<string, SummaryObject> = {};
  private protocolTree: Record<string, SummaryObject> = {};
  private customTrees: Record<string, ISummaryTree> = {};
  
  addAppBlob(path: string, content: string | ArrayBuffer): this {
    this.addToTree(this.appTree, path, {
      type: SummaryType.Blob,
      content
    });
    return this;
  }
  
  addProtocolBlob(path: string, content: string | ArrayBuffer): this {
    this.addToTree(this.protocolTree, path, {
      type: SummaryType.Blob,
      content
    });
    return this;
  }
  
  addCustomTree(name: string, tree: ISummaryTree): this {
    this.customTrees[name] = tree;
    return this;
  }
  
  async addAppBlobFromStorage(
    path: string, 
    storage: Pick<IDocumentStorageService, "readBlob">, 
    blobId: string
  ): Promise<this> {
    const content = await storage.readBlob(blobId);
    return this.addAppBlob(path, content);
  }
  
  buildCombined(): CombinedAppAndProtocolSummary {
    const tree: Record<string, ISummaryTree> = {
      ".app": {
        type: SummaryType.Tree,
        tree: this.appTree
      },
      ".protocol": {
        type: SummaryType.Tree,
        tree: this.protocolTree
      },
      ...this.customTrees
    };
    
    return {
      type: SummaryType.Tree,
      tree
    } as CombinedAppAndProtocolSummary;
  }
  
  buildApp(): ISummaryTree {
    return {
      type: SummaryType.Tree,
      tree: this.appTree
    };
  }
  
  buildProtocol(): ISummaryTree {
    return {
      type: SummaryType.Tree,
      tree: this.protocolTree
    };
  }
  
  private addToTree(
    tree: Record<string, SummaryObject>, 
    path: string, 
    object: SummaryObject
  ): void {
    const parts = path.split('/');
    let current = tree;
    
    for (let i = 0; i < parts.length - 1; i++) {
      const part = parts[i];
      if (!current[part]) {
        current[part] = {
          type: SummaryType.Tree,
          tree: {}
        };
      }
      current = (current[part] as ISummaryTree).tree;
    }
    
    current[parts[parts.length - 1]] = object;
  }
  
  clear(): this {
    this.appTree = {};
    this.protocolTree = {};
    this.customTrees = {};
    return this;
  }
}

// Usage
const builder = new SummaryBuilder();
const summary = builder
  .addAppBlob("data.json", JSON.stringify({ key: "value" }))
  .addProtocolBlob("attributes", JSON.stringify(documentAttributes))
  .addProtocolBlob("quorum/key1", JSON.stringify(quorumValue))
  .buildCombined();

Data Cache Manager

import { readAndParse } from "@fluidframework/driver-utils";

class DataCacheManager {
  private cache = new Map<string, { data: any; expiry: number }>();
  private pendingRequests = new Map<string, Promise<any>>();
  
  constructor(
    private storage: Pick<IDocumentStorageService, "readBlob">,
    private defaultTtlMs: number = 300000 // 5 minutes
  ) {}
  
  async get<T>(blobId: string, ttlMs: number = this.defaultTtlMs): Promise<T> {
    // Check cache first
    const cached = this.cache.get(blobId);
    if (cached && Date.now() < cached.expiry) {
      return cached.data;
    }
    
    // Check if request is already pending
    const pending = this.pendingRequests.get(blobId);
    if (pending) {
      return pending;
    }
    
    // Make new request
    const promise = this.fetchAndCache<T>(blobId, ttlMs);
    this.pendingRequests.set(blobId, promise);
    
    try {
      const result = await promise;
      return result;
    } finally {
      this.pendingRequests.delete(blobId);
    }
  }
  
  private async fetchAndCache<T>(blobId: string, ttlMs: number): Promise<T> {
    const data = await readAndParse<T>(this.storage, blobId);
    
    this.cache.set(blobId, {
      data,
      expiry: Date.now() + ttlMs
    });
    
    return data;
  }
  
  invalidate(blobId: string): void {
    this.cache.delete(blobId);
    this.pendingRequests.delete(blobId);
  }
  
  clear(): void {
    this.cache.clear();
    this.pendingRequests.clear();
  }
  
  prune(): void {
    const now = Date.now();
    for (const [key, value] of this.cache.entries()) {
      if (now >= value.expiry) {
        this.cache.delete(key);
      }
    }
  }
  
  get size(): number {
    return this.cache.size;
  }
}