Commit f4a8a351 authored by Robert Knight's avatar Robert Knight

Wait for feature flag state before sending document info in VitalSource

In order to facilitate a seamless transition after a chapter navigation in
VitalSource, it will be necessary for the Guest in the new frame to report
document information based on the same `book_as_single_document` feature flag
state as for the previous frame.

Allow integrations to signal whether the Guest should wait to receive feature
flags from the sidebar before sending document info via `documentInfoChanged`,
and enable this for VitalSource. We don't enable this for all integrations
because it adds latency on the critical path to fetching and displaying
annotations.
parent 10e81814
...@@ -135,6 +135,9 @@ export class Guest implements Annotator, Destroyable { ...@@ -135,6 +135,9 @@ export class Guest implements Annotator, Destroyable {
public features: FeatureFlags; public features: FeatureFlags;
/** Promise that resolves when feature flags are received from the sidebar. */
private _featureFlagsReceived: Promise<void>;
private _adder: Adder; private _adder: Adder;
private _clusterToolbar?: HighlightClusterController; private _clusterToolbar?: HighlightClusterController;
private _hostFrame: Window; private _hostFrame: Window;
...@@ -231,12 +234,12 @@ export class Guest implements Annotator, Destroyable { ...@@ -231,12 +234,12 @@ export class Guest implements Annotator, Destroyable {
}); });
this.features = new FeatureFlags(); this.features = new FeatureFlags();
this._featureFlagsReceived = new Promise(resolve => {
this.features.on('flagsChanged', resolve);
});
this._integration = createIntegration(this); this._integration = createIntegration(this);
this._integration.on('uriChanged', async () => { this._integration.on('uriChanged', () => this._sendDocumentInfo());
const metadata = await this.getDocumentInfo();
this._sidebarRPC.call('documentInfoChanged', metadata);
});
if (config.contentInfoBanner) { if (config.contentInfoBanner) {
this._integration.showContentInfo?.(config.contentInfoBanner); this._integration.showContentInfo?.(config.contentInfoBanner);
} }
...@@ -344,6 +347,15 @@ export class Guest implements Annotator, Destroyable { ...@@ -344,6 +347,15 @@ export class Guest implements Annotator, Destroyable {
}; };
} }
/** Send the current document URI and metadata to the sidebar. */
async _sendDocumentInfo() {
if (this._integration.waitForFeatureFlags?.()) {
await this._featureFlagsReceived;
}
const metadata = await this.getDocumentInfo();
this._sidebarRPC.call('documentInfoChanged', metadata);
}
/** /**
* Shift the position of the adder on window 'resize' events * Shift the position of the adder on window 'resize' events
*/ */
...@@ -456,9 +468,8 @@ export class Guest implements Annotator, Destroyable { ...@@ -456,9 +468,8 @@ export class Guest implements Annotator, Destroyable {
this._portFinder.discover('sidebar').then(port => { this._portFinder.discover('sidebar').then(port => {
this._sidebarRPC.connect(port); this._sidebarRPC.connect(port);
}); });
this.getDocumentInfo().then(metadata =>
this._sidebarRPC.call('documentInfoChanged', metadata) this._sendDocumentInfo();
);
} }
destroy() { destroy() {
......
...@@ -272,7 +272,12 @@ describe('annotator/integrations/vitalsource', () => { ...@@ -272,7 +272,12 @@ describe('annotator/integrations/vitalsource', () => {
it('allows annotation', () => { it('allows annotation', () => {
const integration = createIntegration(); const integration = createIntegration();
assert.equal(integration.canAnnotate(), true); assert.isTrue(integration.canAnnotate());
});
it('asks guest to wait for feature flags before sending document info', () => {
const integration = createIntegration();
assert.isTrue(integration.waitForFeatureFlags());
}); });
it('delegates to HTMLIntegration for side-by-side mode', () => { it('delegates to HTMLIntegration for side-by-side mode', () => {
......
...@@ -582,4 +582,15 @@ export class VitalSourceContentIntegration ...@@ -582,4 +582,15 @@ export class VitalSourceContentIntegration
_bookIsSingleDocument(): boolean { _bookIsSingleDocument(): boolean {
return this._features.flagEnabled('book_as_single_document'); return this._features.flagEnabled('book_as_single_document');
} }
waitForFeatureFlags() {
// The `book_as_single_document` flag changes the URI reported by this
// integration.
//
// Ask the guest to delay reporting document metadata to the sidebar until
// feature flags have been received. This ensures that the initial document
// info reported to the sidebar after a chapter navigation is consistent
// between the previous/new guest frames.
return true;
}
} }
...@@ -1433,6 +1433,21 @@ describe('Guest', () => { ...@@ -1433,6 +1433,21 @@ describe('Guest', () => {
}); });
}); });
it('waits for feature flags before sending metadata if requested by integration', async () => {
fakeIntegration.waitForFeatureFlags = () => true;
createGuest();
await delay(0);
assert.isFalse(sidebarRPC().call.calledWith('documentInfoChanged'));
emitSidebarEvent('featureFlagsUpdated', {
book_as_single_document: true,
});
await delay(0);
assert.isTrue(sidebarRPC().call.calledWith('documentInfoChanged'));
});
it('sends segment info to sidebar when available', async () => { it('sends segment info to sidebar when available', async () => {
fakeIntegration.uri.resolves('https://bookstore.com/books/1234'); fakeIntegration.uri.resolves('https://bookstore.com/books/1234');
fakeIntegration.getMetadata.resolves({ title: 'A little book' }); fakeIntegration.getMetadata.resolves({ title: 'A little book' });
......
...@@ -221,6 +221,12 @@ export type IntegrationBase = { ...@@ -221,6 +221,12 @@ export type IntegrationBase = {
scrollToAnchor(a: Anchor): Promise<void>; scrollToAnchor(a: Anchor): Promise<void>;
/** Show information about the current document and content provider */ /** Show information about the current document and content provider */
showContentInfo?(config: ContentInfoConfig): void; showContentInfo?(config: ContentInfoConfig): void;
/**
* Whether the Guest should wait for feature flags to be received from the
* sidebar before sending initial document info to the sidebar.
*/
waitForFeatureFlags?(): boolean;
}; };
export type Integration = Destroyable & TinyEmitter & IntegrationBase; export type Integration = Destroyable & TinyEmitter & IntegrationBase;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment