CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-fluidframework--driver-utils

Collection of utility functions for Fluid drivers

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

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;
  }
}

docs

data-processing-summary.md

index.md

network-utilities.md

protocol-message-utilities.md

request-management.md

retry-rate-limiting.md

storage-services.md

tree-blob-utilities.md

url-compression.md

tile.json