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