Storage service implementations using proxy patterns with optional intelligent prefetching. Provides abstraction layers for document storage operations.
Base proxy implementation that delegates all operations to an internal storage service while maintaining the same interface.
/**
* Proxy pattern implementation for IDocumentStorageService
* Delegates all operations to internal storage service
*/
class DocumentStorageServiceProxy implements IDocumentStorageService {
/**
* @param internalStorageService - The underlying storage service to proxy
*/
constructor(internalStorageService: IDocumentStorageService);
/**
* Get or set storage service policies
*/
get policies(): IDocumentStorageServicePolicies | undefined;
set policies(policies: IDocumentStorageServicePolicies | undefined);
/**
* Retrieves snapshot tree for specified version
* @param version - Optional version to retrieve
* @param scenarioName - Optional scenario name for telemetry
* @returns Promise resolving to snapshot tree or null
*/
getSnapshotTree(
version?: IVersion,
scenarioName?: string
): Promise<ISnapshotTree | null>;
/**
* Retrieves complete snapshot including tree and blobs
* @param snapshotFetchOptions - Options for snapshot fetching
* @returns Promise resolving to complete snapshot
*/
getSnapshot(snapshotFetchOptions?: ISnapshotFetchOptions): Promise<ISnapshot>;
/**
* Retrieves version history for the document
* @param versionId - Starting version ID (null for latest)
* @param count - Number of versions to retrieve
* @param scenarioName - Optional scenario name for telemetry
* @param fetchSource - Optional fetch source specification
* @returns Promise resolving to array of versions
*/
getVersions(
versionId: string | null,
count: number,
scenarioName?: string,
fetchSource?: FetchSource
): Promise<IVersion[]>;
/**
* Uploads summary with context information
* @param summary - Summary tree to upload
* @param context - Upload context and metadata
* @returns Promise resolving to summary handle string
*/
uploadSummaryWithContext(
summary: ISummaryTree,
context: ISummaryContext
): Promise<string>;
/**
* Downloads summary for specified version
* @param handle - Summary handle to download
* @returns Promise resolving to summary tree
*/
downloadSummary(handle: ISummaryHandle): Promise<ISummaryTree>;
/**
* Creates a new blob in storage
* @param file - Blob content as ArrayBufferLike
* @returns Promise resolving to blob creation response
*/
createBlob(file: ArrayBufferLike): Promise<ICreateBlobResponse>;
/**
* Reads blob content by ID
* @param id - Blob ID to retrieve
* @returns Promise resolving to blob content
*/
readBlob(id: string): Promise<ArrayBufferLike>;
}Usage Example:
import { DocumentStorageServiceProxy } from "@fluidframework/driver-utils";
// Create proxy around existing storage service
const storageProxy = new DocumentStorageServiceProxy(originalStorageService);
// Configure policies
storageProxy.policies = {
maximumCacheDurationMs: 60000,
caching: LoaderCachingPolicy.Prefetch
};
// Use like any storage service
const snapshot = await storageProxy.getSnapshot({
loadingGroupId: "initial-load",
scenarioName: "app-load"
});
const versions = await storageProxy.getVersions(null, 10);Enhanced storage service that implements intelligent blob prefetching based on snapshot tree structure to improve performance.
/**
* Enhanced storage service with intelligent blob prefetching
* Extends DocumentStorageServiceProxy with prefetching capabilities
*/
class PrefetchDocumentStorageService extends DocumentStorageServiceProxy {
/**
* Disable prefetching and clear any cached prefetch data
* Call this to stop background prefetching operations
*/
stopPrefetch(): void;
}Usage Example:
import { PrefetchDocumentStorageService } from "@fluidframework/driver-utils";
// Create prefetching storage service
const prefetchStorage = new PrefetchDocumentStorageService(baseStorageService);
// The service automatically prefetches:
// 1. Metadata blobs (prefixed with '.', 'header', 'quorum*') immediately
// 2. Other blobs in the background based on access patterns
const snapshot = await prefetchStorage.getSnapshot();
// Blobs are likely already prefetched and cached
const headerBlob = await prefetchStorage.readBlob(headerBlobId); // Fast!
const dataBlob = await prefetchStorage.readBlob(dataBlobId); // May be prefetched
// Clean up when done
prefetchStorage.stopPrefetch();Create custom proxy implementations for specific needs:
import { DocumentStorageServiceProxy } from "@fluidframework/driver-utils";
class LoggingStorageServiceProxy extends DocumentStorageServiceProxy {
constructor(
internalService: IDocumentStorageService,
private logger: ITelemetryLoggerExt
) {
super(internalService);
}
async getSnapshot(options?: ISnapshotFetchOptions): Promise<ISnapshot> {
const start = performance.now();
try {
const result = await super.getSnapshot(options);
this.logger.sendTelemetryEvent({
eventName: "GetSnapshot",
duration: performance.now() - start,
blobCount: Object.keys(result.blobs).length,
treeNodeCount: this.countTreeNodes(result.snapshotTree)
});
return result;
} catch (error) {
this.logger.sendErrorEvent({
eventName: "GetSnapshotFailed",
duration: performance.now() - start
}, error);
throw error;
}
}
async readBlob(id: string): Promise<ArrayBufferLike> {
const start = performance.now();
try {
const result = await super.readBlob(id);
this.logger.sendTelemetryEvent({
eventName: "ReadBlob",
blobId: id,
blobSizeBytes: result.byteLength,
duration: performance.now() - start
});
return result;
} catch (error) {
this.logger.sendErrorEvent({
eventName: "ReadBlobFailed",
blobId: id,
duration: performance.now() - start
}, error);
throw error;
}
}
private countTreeNodes(tree: ISnapshotTree): number {
let count = 1; // Current node
for (const [, subtree] of Object.entries(tree.trees)) {
count += this.countTreeNodes(subtree);
}
return count;
}
}Implement caching on top of the base proxy:
import { DocumentStorageServiceProxy } from "@fluidframework/driver-utils";
class CachingStorageServiceProxy extends DocumentStorageServiceProxy {
private blobCache = new Map<string, ArrayBufferLike>();
private snapshotCache = new Map<string, ISnapshot>();
constructor(
internalService: IDocumentStorageService,
private maxCacheSize: number = 100
) {
super(internalService);
}
async readBlob(id: string): Promise<ArrayBufferLike> {
// Check cache first
const cached = this.blobCache.get(id);
if (cached) {
return cached;
}
// Fetch from underlying service
const blob = await super.readBlob(id);
// Cache with size limit
if (this.blobCache.size >= this.maxCacheSize) {
// Simple LRU: remove first entry
const firstKey = this.blobCache.keys().next().value;
this.blobCache.delete(firstKey);
}
this.blobCache.set(id, blob);
return blob;
}
async getSnapshot(options?: ISnapshotFetchOptions): Promise<ISnapshot> {
const cacheKey = JSON.stringify(options || {});
const cached = this.snapshotCache.get(cacheKey);
if (cached) {
return cached;
}
const snapshot = await super.getSnapshot(options);
// Cache with size limit
if (this.snapshotCache.size >= 10) {
const firstKey = this.snapshotCache.keys().next().value;
this.snapshotCache.delete(firstKey);
}
this.snapshotCache.set(cacheKey, snapshot);
return snapshot;
}
clearCache(): void {
this.blobCache.clear();
this.snapshotCache.clear();
}
}Combine with retry logic for robust operations:
import {
DocumentStorageServiceProxy,
runWithRetry
} from "@fluidframework/driver-utils";
class RetryStorageServiceProxy extends DocumentStorageServiceProxy {
constructor(
internalService: IDocumentStorageService,
private logger: ITelemetryLoggerExt
) {
super(internalService);
}
async readBlob(id: string): Promise<ArrayBufferLike> {
return runWithRetry(
async (signal) => {
return super.readBlob(id);
},
`readBlob_${id}`,
this.logger,
{ cancel: AbortSignal.timeout(30000) }
);
}
async getSnapshot(options?: ISnapshotFetchOptions): Promise<ISnapshot> {
return runWithRetry(
async (signal) => {
return super.getSnapshot(options);
},
"getSnapshot",
this.logger,
{
cancel: AbortSignal.timeout(60000),
onRetry: (delay, error) => {
this.logger.sendTelemetryEvent({
eventName: "GetSnapshotRetry",
retryDelayMs: delay,
error: error.message
});
}
}
);
}
}Handle multiple tenants with a single proxy:
import { DocumentStorageServiceProxy } from "@fluidframework/driver-utils";
class MultiTenantStorageServiceProxy extends DocumentStorageServiceProxy {
private tenantServices = new Map<string, IDocumentStorageService>();
private currentTenantId?: string;
constructor(private createTenantService: (tenantId: string) => IDocumentStorageService) {
// Initialize with no-op service
super({
getSnapshot: async () => { throw new Error("No tenant selected"); },
readBlob: async () => { throw new Error("No tenant selected"); },
// ... other required methods
} as IDocumentStorageService);
}
setTenant(tenantId: string): void {
this.currentTenantId = tenantId;
let service = this.tenantServices.get(tenantId);
if (!service) {
service = this.createTenantService(tenantId);
this.tenantServices.set(tenantId, service);
}
// Update internal service
(this as any).internalStorageService = service;
}
async readBlob(id: string): Promise<ArrayBufferLike> {
if (!this.currentTenantId) {
throw new Error("No tenant selected");
}
return super.readBlob(id);
}
// Override other methods as needed with tenant validation
}