Skip to content

Commit 1078787

Browse files
committed
fix(docs): synchronize Worker session baselines
1 parent b566935 commit 1078787

3 files changed

Lines changed: 150 additions & 20 deletions

File tree

packages/docs-layout-worker/src/index.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export class DocsLayoutWorkerClientService extends Disposable implements IDocLay
6262
private _channelService: ChannelService | null = null;
6363
private _runtime: IDocsLayoutWorkerRuntime | null = null;
6464
private _initialization: Promise<void>;
65+
private _initialized = false;
6566
private readonly _performanceTracker = new DocsLayoutWorkerPerformanceTracker();
6667

6768
constructor(
@@ -85,9 +86,14 @@ export class DocsLayoutWorkerClientService extends Disposable implements IDocLay
8586
await this._initialization;
8687
}
8788

88-
async createSession(request: IDocLayoutCreateSessionRequest): Promise<void> {
89-
await this._initialization;
90-
return this._withTimeout(this._getRuntime().createSession(request), 'create session');
89+
createSession(request: IDocLayoutCreateSessionRequest): Promise<void> {
90+
if (this._initialized) {
91+
return this._withTimeout(this._getRuntime().createSession(request), 'create session');
92+
}
93+
94+
const capturedRequest = Tools.deepClone(request);
95+
return this._initialization.then(() =>
96+
this._withTimeout(this._getRuntime().createSession(capturedRequest), 'create session'));
9197
}
9298

9399
async startLayout(request: IDocLayoutStartRequest): Promise<IDocLayoutStartResult> {
@@ -152,6 +158,7 @@ export class DocsLayoutWorkerClientService extends Disposable implements IDocLay
152158
}
153159

154160
private async _replaceRuntime(): Promise<void> {
161+
this._initialized = false;
155162
this._disposeRuntime();
156163
const worker = this._workerFactory();
157164
const channelService = new ChannelService(createDocsLayoutWorkerMessageProtocol(worker, this._performanceTracker));
@@ -161,9 +168,11 @@ export class DocsLayoutWorkerClientService extends Disposable implements IDocLay
161168
channelService.requestChannel(DOCS_LAYOUT_WORKER_CHANNEL)
162169
);
163170
await this._withTimeout(this._verifyCapabilities(), 'verify capabilities');
171+
this._initialized = true;
164172
}
165173

166174
private _disposeRuntime(): void {
175+
this._initialized = false;
167176
this._channelService?.dispose();
168177
this._channelService = null;
169178
this._runtime = null;

packages/docs/src/services/__tests__/doc-layout-executor.service.spec.ts

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,90 @@ describe('DocLayoutExecutorService', () => {
242242
});
243243
});
244244

245+
it('folds mutations committed before a deferred snapshot transfer into one consistent Worker baseline', async () => {
246+
const injector = univer.__getInjector();
247+
const service = injector.get(DocLayoutExecutorService);
248+
const executor = createExecutor();
249+
service.register(executor);
250+
univer.createUnit<IDocumentData, DocumentDataModel>(
251+
UniverInstanceType.UNIVER_DOC,
252+
createDocumentData('traditional-doc', DocumentFlavor.TRADITIONAL)
253+
);
254+
255+
const textX = new TextX();
256+
textX.push({ t: TextXActionType.RETAIN, len: 5 });
257+
textX.push({
258+
t: TextXActionType.INSERT,
259+
len: 1,
260+
body: { dataStream: 'A' },
261+
});
262+
injector.get(ICommandService).syncExecuteCommand(RichTextEditingMutation.id, {
263+
unitId: 'traditional-doc',
264+
actions: JSONX.getInstance().editOp(textX.serialize(), ['body']),
265+
textRanges: [],
266+
});
267+
268+
await service.startLayout(createMountIdentity(), { reason: 'edit', anchor: 6 }, 32);
269+
270+
expect(executor.createSession).toHaveBeenCalledTimes(1);
271+
expect(executor.createSession).toHaveBeenCalledWith(expect.objectContaining({
272+
modelRevision: 1,
273+
snapshot: expect.objectContaining({
274+
body: expect.objectContaining({ dataStream: 'HelloA\r\n' }),
275+
}),
276+
}));
277+
expect(executor.startLayout).toHaveBeenCalledWith(expect.objectContaining({
278+
baseRevision: 1,
279+
modelRevision: 1,
280+
mutations: [],
281+
}));
282+
});
283+
284+
it('rebases the deferred Worker snapshot after executor initialization completes', async () => {
285+
const injector = univer.__getInjector();
286+
const service = injector.get(DocLayoutExecutorService);
287+
const executor = createExecutor();
288+
let resolveInitialization: () => void = () => {};
289+
vi.mocked(executor.initialize).mockImplementation(() => new Promise<void>((resolve) => {
290+
resolveInitialization = resolve;
291+
}));
292+
service.register(executor);
293+
univer.createUnit<IDocumentData, DocumentDataModel>(
294+
UniverInstanceType.UNIVER_DOC,
295+
createDocumentData('traditional-doc', DocumentFlavor.TRADITIONAL)
296+
);
297+
await new Promise<void>((resolve) => setTimeout(resolve, 20));
298+
expect(executor.createSession).not.toHaveBeenCalled();
299+
300+
const textX = new TextX();
301+
textX.push({ t: TextXActionType.RETAIN, len: 5 });
302+
textX.push({
303+
t: TextXActionType.INSERT,
304+
len: 1,
305+
body: { dataStream: 'A' },
306+
});
307+
injector.get(ICommandService).syncExecuteCommand(RichTextEditingMutation.id, {
308+
unitId: 'traditional-doc',
309+
actions: JSONX.getInstance().editOp(textX.serialize(), ['body']),
310+
textRanges: [],
311+
});
312+
resolveInitialization();
313+
314+
await service.startLayout(createMountIdentity(), { reason: 'edit', anchor: 6 }, 32);
315+
316+
expect(executor.createSession).toHaveBeenCalledWith(expect.objectContaining({
317+
modelRevision: 1,
318+
snapshot: expect.objectContaining({
319+
body: expect.objectContaining({ dataStream: 'HelloA\r\n' }),
320+
}),
321+
}));
322+
expect(executor.startLayout).toHaveBeenCalledWith(expect.objectContaining({
323+
baseRevision: 1,
324+
modelRevision: 1,
325+
mutations: [],
326+
}));
327+
});
328+
245329
it('reports capability initialization failures and unregisters the executor', async () => {
246330
const service = univer.__getInjector().get(DocLayoutExecutorService);
247331
const executor = createExecutor();
@@ -325,7 +409,7 @@ describe('DocLayoutExecutorService', () => {
325409
UniverInstanceType.UNIVER_DOC,
326410
createDocumentData('traditional-doc', DocumentFlavor.TRADITIONAL)
327411
);
328-
await Promise.resolve();
412+
await vi.waitFor(() => expect(executor.createSession).toHaveBeenCalledTimes(1));
329413

330414
const commandService = injector.get(ICommandService);
331415
for (const [offset, text] of [[5, 'A'], [6, 'B']] as const) {
@@ -445,7 +529,7 @@ describe('DocLayoutExecutorService', () => {
445529
UniverInstanceType.UNIVER_DOC,
446530
createDocumentData('traditional-doc', DocumentFlavor.TRADITIONAL)
447531
);
448-
await Promise.resolve();
532+
await vi.waitFor(() => expect(executor.createSession).toHaveBeenCalledTimes(1));
449533

450534
const commandService = injector.get(ICommandService);
451535
const pastedText = '大段粘贴内容'.repeat(1_024);

packages/docs/src/services/doc-layout-executor.service.ts

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ export interface IDocLayoutExecutor {
140140
readonly type: DocLayoutExecutorType;
141141
initialize(): Promise<void>;
142142
recover(): Promise<void>;
143+
/**
144+
* Capture the request synchronously before returning. The source document model remains mutable
145+
* while the returned promise tracks remote session creation.
146+
*/
143147
createSession(request: IDocLayoutCreateSessionRequest): Promise<void>;
144148
startLayout(request: IDocLayoutStartRequest): Promise<IDocLayoutStartResult>;
145149
stepLayout(request: IDocLayoutStepRequest): Promise<IDocLayoutStepResult>;
@@ -210,6 +214,7 @@ export interface IDocLayoutStartOptions {
210214
export class DocLayoutExecutorService extends Disposable {
211215
private static readonly _performanceSampleLimit = 2048;
212216
private _executor: Nullable<IDocLayoutExecutor> = null;
217+
private _executorReady: Promise<void> | null = null;
213218
private _recoveryFailure: { diagnostic: string; unitId: string } | null = null;
214219
private _sessionEpoch = 0;
215220
private readonly _sessions = new Map<string, IDocLayoutManagedSession>();
@@ -278,7 +283,9 @@ export class DocLayoutExecutorService extends Disposable {
278283
diagnostic: null,
279284
recoveryUnitId: null,
280285
});
281-
executor.initialize().then(() => {
286+
const executorReady = executor.initialize();
287+
this._executorReady = executorReady;
288+
executorReady.then(() => {
282289
if (this._executor === executor) {
283290
this._executorStatus$.next({
284291
state: DocLayoutExecutorState.ACTIVE,
@@ -292,6 +299,7 @@ export class DocLayoutExecutorService extends Disposable {
292299
return;
293300
}
294301
this._executor = null;
302+
this._executorReady = null;
295303
this._disposeManagedSessions();
296304
this._sessions.clear();
297305
const diagnostic = error instanceof Error ? error.message : String(error);
@@ -310,6 +318,7 @@ export class DocLayoutExecutorService extends Disposable {
310318
return toDisposable(() => {
311319
if (this._executor === executor) {
312320
this._executor = null;
321+
this._executorReady = null;
313322
this._disposeManagedSessions();
314323
this._sessions.clear();
315324
this._executorStatus$.next({
@@ -364,10 +373,13 @@ export class DocLayoutExecutorService extends Disposable {
364373
this._hydrationSamples.clear();
365374

366375
try {
367-
await executor.recover();
376+
const executorReady = executor.recover();
377+
this._executorReady = executorReady;
378+
await executorReady;
368379
} catch (error) {
369380
const recoveryDiagnostic = error instanceof Error ? error.message : String(error);
370381
this._executor = null;
382+
this._executorReady = null;
371383
this._recoveryFailure = { diagnostic: recoveryDiagnostic, unitId };
372384
this._executorStatus$.next({
373385
state: DocLayoutExecutorState.RECOVERING,
@@ -468,6 +480,7 @@ export class DocLayoutExecutorService extends Disposable {
468480
}
469481
this._sessions.clear();
470482
this._executor = null;
483+
this._executorReady = null;
471484
this._recoveryFailure = null;
472485
this._executorStatus$.complete();
473486
super.dispose();
@@ -606,16 +619,7 @@ export class DocLayoutExecutorService extends Disposable {
606619
disposed: false,
607620
cancelPendingCreateTask: null,
608621
};
609-
const request: IDocLayoutCreateSessionRequest = {
610-
unitId,
611-
sessionEpoch: session.sessionEpoch,
612-
snapshot: dataModel.getSnapshot(),
613-
modelRevision,
614-
locale: this._localeService.getCurrentLocale(),
615-
localeData: this._localeService.getLocales(),
616-
direction: this._localeService.getDirection(),
617-
};
618-
const createTask = this._createSessionOnNextMacroTask(executor, session, request);
622+
const createTask = this._createSessionOnNextMacroTask(executor, session, dataModel);
619623
session.queue = createTask;
620624
this._sessions.set(unitId, session);
621625
try {
@@ -685,20 +689,53 @@ export class DocLayoutExecutorService extends Disposable {
685689
private _createSessionOnNextMacroTask(
686690
executor: IDocLayoutExecutor,
687691
session: IDocLayoutManagedSession,
688-
request: IDocLayoutCreateSessionRequest
692+
dataModel: DocumentDataModel
689693
): Promise<void> {
690694
return new Promise<void>((resolve, reject) => {
691695
const cancelTask = requestImmediateMacroTask(() => {
692696
session.cancelPendingCreateTask = null;
697+
const unitId = dataModel.getUnitId();
693698
if (
694699
session.disposed ||
695700
this._executor !== executor ||
696-
this._sessions.get(request.unitId) !== session
701+
this._sessions.get(unitId) !== session
697702
) {
698703
resolve();
699704
return;
700705
}
701-
executor.createSession(request).then(resolve, reject);
706+
707+
const executorReady = this._executorReady;
708+
if (executorReady == null) {
709+
resolve();
710+
return;
711+
}
712+
executorReady.then(() => {
713+
if (
714+
session.disposed ||
715+
this._executor !== executor ||
716+
this._sessions.get(unitId) !== session ||
717+
this._getEligibleModel(unitId) !== dataModel
718+
) {
719+
resolve();
720+
return;
721+
}
722+
723+
// Mutations committed before the deferred transfer are already present in the
724+
// live model snapshot. Fold them into the baseline instead of replaying them.
725+
session.sourceMutationRevision = dataModel.getMutationRevision();
726+
session.workerRevision = session.modelRevision;
727+
session.pendingMutations.length = 0;
728+
const request: IDocLayoutCreateSessionRequest = {
729+
unitId,
730+
sessionEpoch: session.sessionEpoch,
731+
snapshot: dataModel.getSnapshot(),
732+
modelRevision: session.modelRevision,
733+
locale: this._localeService.getCurrentLocale(),
734+
localeData: this._localeService.getLocales(),
735+
direction: this._localeService.getDirection(),
736+
};
737+
executor.createSession(request).then(resolve, reject);
738+
}, reject);
702739
});
703740
session.cancelPendingCreateTask = () => {
704741
cancelTask();

0 commit comments

Comments
 (0)