Collection of utility functions for Fluid drivers
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Utilities for parsing JSON data from storage and managing summary structures. Provides type-safe data processing and summary format handling.
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"
);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 };
}
}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"
);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;
}>;
}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();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;
}
}