Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .changeset/ready-peaches-juggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
"@fluidframework/odsp-driver": minor
"__section": feature
---
Enable point-in-time loading on the standard ODSP document service factory

[`OdspDocumentServiceFactoryCore`](https://fluidframework.com/docs/api/odsp-driver/odspdocumentservicefactorycore-class)
now exposes the optional `createPointInTimeDocumentService` capability.
[`OdspDocumentServiceFactory`](https://fluidframework.com/docs/api/odsp-driver/odspdocumentservicefactory-class)
inherits this capability, so hosts can use the standard factory with
[`loadContainerToSequenceNumber`](https://fluidframework.com/docs/api/container-loader/loadcontainertosequencenumber-function)
for sequence-number-based document loading. Factories that do not support point-in-time loading
leave the capability undefined.

```typescript
const factory = new OdspDocumentServiceFactory(getStorageToken, getWebsocketToken);

if (factory.createPointInTimeDocumentService !== undefined) {
const documentService = await factory.createPointInTimeDocumentService(
resolvedUrl,
targetSequenceNumber,
);
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ export class OdspDocumentServiceFactoryCore implements IDocumentServiceFactory,
createDocumentService(resolvedUrl: IResolvedUrl, logger?: ITelemetryBaseLogger, clientIsSummarizer?: boolean): Promise<IDocumentService>;
// (undocumented)
protected createDocumentServiceCore(resolvedUrl: IResolvedUrl, odspLogger: ITelemetryBaseLogger, cacheAndTrackerArg?: ICacheAndTracker, clientIsSummarizer?: boolean): Promise<IDocumentService>;
readonly createPointInTimeDocumentService?: IPointInTimeDocumentServiceFactory["createPointInTimeDocumentService"];
getRelayServiceSessionInfo(resolvedUrl: IResolvedUrl): Promise<ISocketStorageDiscovery | undefined>;
readonly ILayerCompatDetails?: unknown;
// (undocumented)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ export class OdspDocumentServiceFactoryCore implements IDocumentServiceFactory,
createDocumentService(resolvedUrl: IResolvedUrl, logger?: ITelemetryBaseLogger, clientIsSummarizer?: boolean): Promise<IDocumentService>;
// (undocumented)
protected createDocumentServiceCore(resolvedUrl: IResolvedUrl, odspLogger: ITelemetryBaseLogger, cacheAndTrackerArg?: ICacheAndTracker, clientIsSummarizer?: boolean): Promise<IDocumentService>;
readonly createPointInTimeDocumentService?: IPointInTimeDocumentServiceFactory["createPointInTimeDocumentService"];
getRelayServiceSessionInfo(resolvedUrl: IResolvedUrl): Promise<ISocketStorageDiscovery | undefined>;
readonly ILayerCompatDetails?: unknown;
// (undocumented)
Expand Down
8 changes: 3 additions & 5 deletions packages/drivers/odsp-driver/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,13 @@ export { prefetchLatestSnapshot } from "./prefetchLatestSnapshot.js";
// Factory
export {
createLocalOdspDocumentServiceFactory,
getOdspPointInTimeDocumentServiceFactory,
OdspDocumentServiceFactory,
} from "./odspDocumentServiceFactory.js";
export { OdspDocumentServiceFactoryCore } from "./odspDocumentServiceFactoryCore.js";
/* eslint-disable import-x/no-internal-modules */
export {
getOdspPointInTimeDocumentServiceFactory,
type IPointInTimeDocumentServiceFactory,
} from "./pointInTimeDriver/odspPointInTimeDocumentServiceFactory.js";
/* eslint-enable import-x/no-internal-modules */
OdspDocumentServiceFactoryCore,
} from "./odspDocumentServiceFactoryCore.js";

// File creation
export { createOdspCreateContainerRequest } from "./createOdspCreateContainerRequest.js";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { LocalOdspDocumentService } from "./localOdspDocumentService.js";
export class LocalOdspDocumentServiceFactory extends OdspDocumentServiceFactoryCore {
private readonly logger: TelemetryLoggerExt = createOdspLogger();

public override readonly createPointInTimeDocumentService = undefined;

constructor(private readonly localSnapshot: Uint8Array | string) {
super(
(_options) => this.throwUnsupportedUsageError("Getting storage token"),
Expand Down
42 changes: 41 additions & 1 deletion packages/drivers/odsp-driver/src/odspDocumentServiceFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import type {

// eslint-disable-next-line import-x/no-internal-modules
import { LocalOdspDocumentServiceFactory } from "./localOdspDriver/localOdspDocumentServiceFactory.js";
import { OdspDocumentServiceFactoryCore } from "./odspDocumentServiceFactoryCore.js";
import {
type IPointInTimeDocumentServiceFactory,
OdspDocumentServiceFactoryCore,
} from "./odspDocumentServiceFactoryCore.js";

/**
* Factory for creating the sharepoint document service. Use this if you want to
Expand All @@ -34,6 +37,43 @@ export class OdspDocumentServiceFactory extends OdspDocumentServiceFactoryCore {
}
}

function isPointInTimeDocumentServiceFactory(
factory: OdspDocumentServiceFactory,
): factory is OdspDocumentServiceFactory & IPointInTimeDocumentServiceFactory {
return typeof factory.createPointInTimeDocumentService === "function";
}

/**
* Creates an ODSP document service factory that supports point-in-time loading.
*
* @param getStorageToken - Fetches storage access tokens.
* @param getWebsocketToken - Fetches websocket access tokens, or `undefined` when unavailable.
* @param persistedCache - Persisted ODSP cache. When omitted, a local in-memory cache is used.
* @param hostPolicy - Host storage policy. When omitted, the default driver policies are used.
* @returns An ODSP document service factory with point-in-time loading capability.
*
* @legacy @beta
*/
export function getOdspPointInTimeDocumentServiceFactory(
getStorageToken: TokenFetcher<OdspResourceTokenFetchOptions>,
getWebsocketToken: TokenFetcher<OdspResourceTokenFetchOptions> | undefined,
persistedCache?: IPersistedCache,
hostPolicy?: HostStoragePolicy,
): IPointInTimeDocumentServiceFactory {
const factory = new OdspDocumentServiceFactory(
getStorageToken,
getWebsocketToken,
persistedCache,
hostPolicy,
);
if (!isPointInTimeDocumentServiceFactory(factory)) {
throw new Error(
"The ODSP document service factory does not support point-in-time loading.",
);
}
return factory;
}

/**
* Creates a factory instance for creating a sharepoint document service from a provided snapshot.
*
Expand Down
179 changes: 177 additions & 2 deletions packages/drivers/odsp-driver/src/odspDocumentServiceFactoryCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from "@fluidframework/driver-utils/internal";
import {
type HostStoragePolicy,
type IOdspResolvedUrl,
type IOdspUrlParts,
type IRelaySessionAwareDriverFactory,
type ISharingLinkKind,
Expand All @@ -29,18 +30,28 @@ import {
type TokenFetchOptions,
type TokenFetcher,
} from "@fluidframework/odsp-driver-definitions/internal";
import { PerformanceEvent, createChildLogger } from "@fluidframework/telemetry-utils/internal";
import {
PerformanceEvent,
UsageError,
createChildLogger,
type TelemetryLoggerExt,
} from "@fluidframework/telemetry-utils/internal";
import { v4 as uuid } from "uuid";

import { useCreateNewModule } from "./createFile/index.js";
import { type ICacheAndTracker, createOdspCacheAndTracker } from "./epochTracker.js";
import {
type EpochTracker,
type ICacheAndTracker,
createOdspCacheAndTracker,
} from "./epochTracker.js";
import {
type INonPersistentCache,
type IPrefetchSnapshotContents,
LocalPersistentCache,
NonPersistentCache,
} from "./odspCache.js";
import { OdspDocumentService } from "./odspDocumentService.js";
import { OdspDriverUrlResolver } from "./odspDriverUrlResolver.js";
import { odspDriverCompatDetailsForLoader } from "./odspLayerCompatState.js";
import {
type IExistingFileInfo,
Expand All @@ -52,6 +63,42 @@ import {
toInstrumentedOdspStorageTokenFetcher,
toInstrumentedOdspTokenFetcher,
} from "./odspUtils.js";
// eslint-disable-next-line import-x/no-internal-modules
import { OdspPointInTimeDocumentService } from "./pointInTimeDriver/odspPointInTimeDocumentService.js";
import {
createOdspVersionManager,
type IOdspVersionManager,
} from "./odspVersionManager/index.js";

/**
* An ODSP document service factory that supports point-in-time (sequence-number-based) loading.
*
* @remarks
* The loader detects this capability structurally, so hosts can pass this factory directly to
* {@link @fluidframework/container-loader#loadContainerToSequenceNumber}.
*
* @legacy @beta
*/
export interface IPointInTimeDocumentServiceFactory extends IDocumentServiceFactory {
/**
* Creates a document service that materializes the document at the requested sequence number.
*
* @param resolvedUrl - The resolved ODSP {@link @fluidframework/driver-definitions#IResolvedUrl}.
* @param targetSequenceNumber - The sequence number at which to materialize the document. See
* {@link @fluidframework/container-loader#ILoadContainerToSequenceNumberProps.loadToSequenceNumber}.
* @param logger - Optional {@link @fluidframework/core-interfaces#ITelemetryBaseLogger}.
* @param clientIsSummarizer - Whether to apply summarizer policies and telemetry to the
* underlying document services. Defaults to `false`.
* @returns A read-only {@link @fluidframework/driver-definitions#IDocumentService} materialized
* at the requested sequence number.
*/
createPointInTimeDocumentService(
resolvedUrl: IResolvedUrl,
targetSequenceNumber: number,
logger?: ITelemetryBaseLogger,
clientIsSummarizer?: boolean,
): Promise<IDocumentService>;
}

/**
* Factory for creating the sharepoint document service. Use this if you want to
Expand Down Expand Up @@ -269,6 +316,134 @@ export class OdspDocumentServiceFactoryCore
);
}

/**
* Creates a document service that reads its snapshot from the closest file version at or before
* the target and its deltas from the live document, materializing a requested sequence number
* through replay.
*
* @param resolvedUrl - The resolved ODSP {@link @fluidframework/driver-definitions#IResolvedUrl}.
* @param targetSequenceNumber - The sequence number at which to materialize the document. See
* {@link @fluidframework/container-loader#ILoadContainerToSequenceNumberProps.loadToSequenceNumber}.
* @param logger - Optional {@link @fluidframework/core-interfaces#ITelemetryBaseLogger}.
* @param clientIsSummarizer - Whether to apply summarizer policies and telemetry to the
* underlying document services. Defaults to `false`.
* @returns A read-only {@link @fluidframework/driver-definitions#IDocumentService} materialized
* at the requested sequence number.
*/
public readonly createPointInTimeDocumentService?: IPointInTimeDocumentServiceFactory["createPointInTimeDocumentService"] =
async (
resolvedUrl: IResolvedUrl,
targetSequenceNumber: number,
logger?: ITelemetryBaseLogger,
clientIsSummarizer?: boolean,
): Promise<IDocumentService> => {
const odspLogger = createOdspLogger(logger);
const extLogger = createChildLogger({ logger: odspLogger });
const odspResolvedUrl = getOdspResolvedUrl(resolvedUrl);

// Use one epoch tracker for version selection, the base snapshot, and live ops so a
// point-in-time load cannot combine data from different file lineages.
const cacheAndTracker = createOdspCacheAndTracker(
this.persistedCache,
new NonPersistentCache(),
{
resolvedUrl: odspResolvedUrl,
docId: odspResolvedUrl.hashedDocumentId,
fileVersion: odspResolvedUrl.fileVersion,
},
extLogger,
clientIsSummarizer,
);

const versionManager = this.createVersionManager(
odspResolvedUrl,
extLogger,
cacheAndTracker.epochTracker,
);
const baseResult = await versionManager.findBaseForSeq(targetSequenceNumber);
if (baseResult.kind === "noBaseVersion") {
const oldestResolvedSequenceDetail =
baseResult.oldestResolvedSeq === undefined
? ""
: ` The oldest resolved file version is at sequence number ${baseResult.oldestResolvedSeq}.`;
throw new UsageError(
`No ODSP file version is available at or before sequence number ${targetSequenceNumber}.${oldestResolvedSequenceDetail}`,
);
}

const recoverableResolvedUrl = await this.resolveFileVersion(
resolvedUrl,
baseResult.base.versionId,
);
// Keep historical snapshots isolated from the normal factory cache while validating both
// services against the shared tracker.
const recoverableDocumentService = await this.createDocumentServiceCore(
recoverableResolvedUrl,
odspLogger,
cacheAndTracker,
clientIsSummarizer,
);
const liveDocumentService = await this.createDocumentServiceCore(
resolvedUrl,
odspLogger,
cacheAndTracker,
clientIsSummarizer,
);
return new OdspPointInTimeDocumentService(
recoverableResolvedUrl,
recoverableDocumentService,
liveDocumentService,
targetSequenceNumber,
);
};

/**
* Creates the version manager used to select the closest file version at or before the target.
*/
private createVersionManager(
odspResolvedUrl: IOdspResolvedUrl,
logger: TelemetryLoggerExt,
epochTracker: EpochTracker,
): IOdspVersionManager {
const urlParts: IOdspUrlParts = {
siteUrl: odspResolvedUrl.siteUrl,
driveId: odspResolvedUrl.driveId,
itemId: odspResolvedUrl.itemId,
};
const getAuthHeader = toInstrumentedOdspStorageTokenFetcher(
logger,
urlParts,
this.getStorageToken,
);
return createOdspVersionManager({
urlParts,
getAuthHeader,
epochTracker,
logger,
});
}

private async resolveFileVersion(
resolvedUrl: IResolvedUrl,
fileVersion: string,
): Promise<IResolvedUrl> {
const odspResolvedUrl = getOdspResolvedUrl(resolvedUrl);
const query = new URLSearchParams({
driveId: odspResolvedUrl.driveId,
itemId: odspResolvedUrl.itemId,
fileVersion,
});
if (odspResolvedUrl.dataStorePath !== undefined) {
query.set("path", odspResolvedUrl.dataStorePath);
}
if (odspResolvedUrl.codeHint?.containerPackageName !== undefined) {
query.set("containerPackageName", odspResolvedUrl.codeHint.containerPackageName);
}
return new OdspDriverUrlResolver().resolve({
url: `${odspResolvedUrl.siteUrl}?${query.toString()}`,
});
}

protected async createDocumentServiceCore(
resolvedUrl: IResolvedUrl,
odspLogger: ITelemetryBaseLogger,
Expand Down
Loading
Loading