refactor store to isolate synthetic tx commits and use markers instead
This commit is contained in:
@@ -116,8 +116,8 @@ export class Store {
|
|||||||
*
|
*
|
||||||
* Builds StoreSnapshot → StoreChange → StoreDelta from the provided
|
* Builds StoreSnapshot → StoreChange → StoreDelta from the provided
|
||||||
* logical before/after element maps and optional appState patches, then
|
* logical before/after element maps and optional appState patches, then
|
||||||
* flushes the resulting durable increment immediately (bypassing the
|
* emits the resulting durable increment immediately through an isolated
|
||||||
* normal componentDidUpdate → store.commit() cycle).
|
* path that does not flush pending micro actions.
|
||||||
*
|
*
|
||||||
* appState patches are merged on top of the current observed appState
|
* appState patches are merged on top of the current observed appState
|
||||||
* baseline so only the provided keys participate in the synthetic diff.
|
* baseline so only the provided keys participate in the synthetic diff.
|
||||||
@@ -167,16 +167,7 @@ export class Store {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.scheduleMicroAction({
|
this.emitIsolatedDurableIncrement(change, delta);
|
||||||
action: CaptureUpdateAction.IMMEDIATELY,
|
|
||||||
change,
|
|
||||||
delta,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Flush immediately so the durable increment is emitted without waiting
|
|
||||||
// for the next componentDidUpdate → store.commit() cycle. This is safe
|
|
||||||
// because commitSyntheticIncrement is never called from within commit().
|
|
||||||
this.flushMicroActions();
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -316,6 +307,24 @@ export class Store {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits a synthetic durable increment immediately without draining the
|
||||||
|
* regular micro-action queue.
|
||||||
|
*/
|
||||||
|
private emitIsolatedDurableIncrement(change: StoreChange, delta: StoreDelta) {
|
||||||
|
const nextSnapshot = this.applyChangeToSnapshot(change);
|
||||||
|
|
||||||
|
if (!nextSnapshot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.emitDurableIncrement(nextSnapshot, change, delta);
|
||||||
|
} finally {
|
||||||
|
this.snapshot = nextSnapshot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs change calculation and emits an ephemeral increment.
|
* Performs change calculation and emits an ephemeral increment.
|
||||||
*
|
*
|
||||||
@@ -563,7 +572,7 @@ export class EphemeralIncrement extends StoreIncrement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serializable semantics marker used to recover pre-tx undo baselines
|
* Serializable delta marker used to recover pre-tx undo baselines
|
||||||
* without rewriting history stack entries.
|
* without rewriting history stack entries.
|
||||||
*/
|
*/
|
||||||
export type TxUndoOverride = {
|
export type TxUndoOverride = {
|
||||||
@@ -575,7 +584,7 @@ export type TxUndoOverride = {
|
|||||||
consumedKey: string;
|
consumedKey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StoreDeltaSemantics = {
|
export type StoreDeltaMarkers = {
|
||||||
txUndoOverrides?: TxUndoOverride[];
|
txUndoOverrides?: TxUndoOverride[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -583,18 +592,18 @@ const cloneTxUndoOverride = (override: TxUndoOverride): TxUndoOverride => ({
|
|||||||
...override,
|
...override,
|
||||||
});
|
});
|
||||||
|
|
||||||
const cloneStoreDeltaSemantics = (
|
const cloneStoreDeltaMarkers = (
|
||||||
semantics: StoreDeltaSemantics | undefined,
|
markers: StoreDeltaMarkers | undefined,
|
||||||
): StoreDeltaSemantics | undefined =>
|
): StoreDeltaMarkers | undefined =>
|
||||||
semantics
|
markers
|
||||||
? {
|
? {
|
||||||
...semantics,
|
...markers,
|
||||||
txUndoOverrides: semantics.txUndoOverrides?.map(cloneTxUndoOverride),
|
txUndoOverrides: markers.txUndoOverrides?.map(cloneTxUndoOverride),
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const pruneStoreDeltaSemantics = (delta: StoreDelta) => {
|
const pruneStoreDeltaMarkers = (delta: StoreDelta) => {
|
||||||
const txUndoOverrides = delta.semantics?.txUndoOverrides;
|
const txUndoOverrides = delta.markers?.txUndoOverrides;
|
||||||
if (!txUndoOverrides || txUndoOverrides.length === 0) {
|
if (!txUndoOverrides || txUndoOverrides.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -616,28 +625,28 @@ const pruneStoreDeltaSemantics = (delta: StoreDelta) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (nextTxUndoOverrides.length === 0) {
|
if (nextTxUndoOverrides.length === 0) {
|
||||||
delta.semantics = undefined;
|
delta.markers = undefined;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
delta.semantics = {
|
delta.markers = {
|
||||||
...delta.semantics,
|
...delta.markers,
|
||||||
txUndoOverrides: nextTxUndoOverrides.map(cloneTxUndoOverride),
|
txUndoOverrides: nextTxUndoOverrides.map(cloneTxUndoOverride),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mergeStoreDeltaSemantics = (
|
export const mergeStoreDeltaMarkers = (
|
||||||
delta: StoreDelta,
|
delta: StoreDelta,
|
||||||
semantics: StoreDeltaSemantics,
|
markers: StoreDeltaMarkers,
|
||||||
) => {
|
) => {
|
||||||
const txUndoOverrides = semantics.txUndoOverrides ?? [];
|
const txUndoOverrides = markers.txUndoOverrides ?? [];
|
||||||
if (txUndoOverrides.length === 0) {
|
if (txUndoOverrides.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const existing = delta.semantics?.txUndoOverrides ?? [];
|
const existing = delta.markers?.txUndoOverrides ?? [];
|
||||||
delta.semantics = {
|
delta.markers = {
|
||||||
...delta.semantics,
|
...delta.markers,
|
||||||
txUndoOverrides: [
|
txUndoOverrides: [
|
||||||
...existing.map(cloneTxUndoOverride),
|
...existing.map(cloneTxUndoOverride),
|
||||||
...txUndoOverrides.map(cloneTxUndoOverride),
|
...txUndoOverrides.map(cloneTxUndoOverride),
|
||||||
@@ -653,7 +662,7 @@ export class StoreDelta {
|
|||||||
public readonly id: string,
|
public readonly id: string,
|
||||||
public readonly elements: ElementsDelta,
|
public readonly elements: ElementsDelta,
|
||||||
public readonly appState: AppStateDelta,
|
public readonly appState: AppStateDelta,
|
||||||
public semantics?: StoreDeltaSemantics,
|
public markers?: StoreDeltaMarkers,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -664,14 +673,14 @@ export class StoreDelta {
|
|||||||
appState: AppStateDelta,
|
appState: AppStateDelta,
|
||||||
opts: {
|
opts: {
|
||||||
id?: string;
|
id?: string;
|
||||||
semantics?: StoreDeltaSemantics;
|
markers?: StoreDeltaMarkers;
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
return new this(
|
return new this(
|
||||||
opts.id ?? randomId(),
|
opts.id ?? randomId(),
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
cloneStoreDeltaSemantics(opts.semantics),
|
cloneStoreDeltaMarkers(opts.markers),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -697,12 +706,12 @@ export class StoreDelta {
|
|||||||
* Restore a store delta instance from a DTO.
|
* Restore a store delta instance from a DTO.
|
||||||
*/
|
*/
|
||||||
public static restore(storeDeltaDTO: DTO<StoreDelta>) {
|
public static restore(storeDeltaDTO: DTO<StoreDelta>) {
|
||||||
const { id, elements, appState, semantics } = storeDeltaDTO;
|
const { id, elements, appState, markers } = storeDeltaDTO;
|
||||||
return new this(
|
return new this(
|
||||||
id,
|
id,
|
||||||
ElementsDelta.restore(elements),
|
ElementsDelta.restore(elements),
|
||||||
AppStateDelta.restore(appState),
|
AppStateDelta.restore(appState),
|
||||||
cloneStoreDeltaSemantics(semantics),
|
cloneStoreDeltaMarkers(markers),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -713,7 +722,7 @@ export class StoreDelta {
|
|||||||
id,
|
id,
|
||||||
elements: { added, removed, updated },
|
elements: { added, removed, updated },
|
||||||
appState: { delta: appStateDelta },
|
appState: { delta: appStateDelta },
|
||||||
semantics,
|
markers,
|
||||||
}: DTO<StoreDelta>) {
|
}: DTO<StoreDelta>) {
|
||||||
const elements = ElementsDelta.create(added, removed, updated);
|
const elements = ElementsDelta.create(added, removed, updated);
|
||||||
const appState = AppStateDelta.create(appStateDelta);
|
const appState = AppStateDelta.create(appStateDelta);
|
||||||
@@ -722,7 +731,7 @@ export class StoreDelta {
|
|||||||
id,
|
id,
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
cloneStoreDeltaSemantics(semantics),
|
cloneStoreDeltaMarkers(markers),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -735,8 +744,10 @@ export class StoreDelta {
|
|||||||
for (const delta of deltas) {
|
for (const delta of deltas) {
|
||||||
aggregatedDelta.elements.squash(delta.elements);
|
aggregatedDelta.elements.squash(delta.elements);
|
||||||
aggregatedDelta.appState.squash(delta.appState);
|
aggregatedDelta.appState.squash(delta.appState);
|
||||||
|
mergeStoreDeltaMarkers(aggregatedDelta, delta.markers ?? {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pruneStoreDeltaMarkers(aggregatedDelta);
|
||||||
return aggregatedDelta;
|
return aggregatedDelta;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -747,7 +758,7 @@ export class StoreDelta {
|
|||||||
const inversed = this.create(
|
const inversed = this.create(
|
||||||
delta.elements.inverse(),
|
delta.elements.inverse(),
|
||||||
delta.appState.inverse(),
|
delta.appState.inverse(),
|
||||||
{ semantics: delta.semantics },
|
{ markers: delta.markers },
|
||||||
);
|
);
|
||||||
return inversed;
|
return inversed;
|
||||||
}
|
}
|
||||||
@@ -794,10 +805,10 @@ export class StoreDelta {
|
|||||||
delta.appState,
|
delta.appState,
|
||||||
{
|
{
|
||||||
id: delta.id,
|
id: delta.id,
|
||||||
semantics: delta.semantics,
|
markers: delta.markers,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
pruneStoreDeltaSemantics(nextDelta);
|
pruneStoreDeltaMarkers(nextDelta);
|
||||||
return nextDelta;
|
return nextDelta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -449,28 +449,27 @@ const stripProps = (
|
|||||||
}, {} as Record<string, any>);
|
}, {} as Record<string, any>);
|
||||||
|
|
||||||
export const checkpointHistory = (history: History, name: string) => {
|
export const checkpointHistory = (history: History, name: string) => {
|
||||||
expect(
|
const normalizeHistoryEntry = (entry: History["undoStack"][number]) => {
|
||||||
history.undoStack.map((x) => ({
|
const { markers, ...rest } = entry;
|
||||||
...x,
|
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
...(markers ? { markers } : {}),
|
||||||
elements: {
|
elements: {
|
||||||
...x.elements,
|
...entry.elements,
|
||||||
added: stripProps(x.elements.added, ["seed", "versionNonce"]),
|
added: stripProps(entry.elements.added, ["seed", "versionNonce"]),
|
||||||
removed: stripProps(x.elements.removed, ["seed", "versionNonce"]),
|
removed: stripProps(entry.elements.removed, ["seed", "versionNonce"]),
|
||||||
updated: stripProps(x.elements.updated, ["seed", "versionNonce"]),
|
updated: stripProps(entry.elements.updated, ["seed", "versionNonce"]),
|
||||||
},
|
},
|
||||||
})),
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
history.undoStack.map(normalizeHistoryEntry),
|
||||||
).toMatchSnapshot(`[${name}] undo stack`);
|
).toMatchSnapshot(`[${name}] undo stack`);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
history.redoStack.map((x) => ({
|
history.redoStack.map(normalizeHistoryEntry),
|
||||||
...x,
|
|
||||||
elements: {
|
|
||||||
...x.elements,
|
|
||||||
added: stripProps(x.elements.added, ["seed", "versionNonce"]),
|
|
||||||
removed: stripProps(x.elements.removed, ["seed", "versionNonce"]),
|
|
||||||
updated: stripProps(x.elements.updated, ["seed", "versionNonce"]),
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
).toMatchSnapshot(`[${name}] redo stack`);
|
).toMatchSnapshot(`[${name}] redo stack`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Delta,
|
Delta,
|
||||||
ElementsDelta,
|
ElementsDelta,
|
||||||
mergeStoreDeltaSemantics,
|
mergeStoreDeltaMarkers,
|
||||||
type StoreDelta,
|
type StoreDelta,
|
||||||
type TxUndoOverride,
|
type TxUndoOverride,
|
||||||
} from "@excalidraw/element";
|
} from "@excalidraw/element";
|
||||||
@@ -62,7 +62,7 @@ export class TransactionManager {
|
|||||||
* Single authoritative lifecycle registry for transactions.
|
* Single authoritative lifecycle registry for transactions.
|
||||||
*
|
*
|
||||||
* We retain ended/canceled metadata after the tx object is released because
|
* We retain ended/canceled metadata after the tx object is released because
|
||||||
* history semantics only persist `txId`; undo/redo still needs to resolve
|
* history markers only persist `txId`; undo/redo still needs to resolve
|
||||||
* whether a tx was active or already ended when applying effective deltas.
|
* whether a tx was active or already ended when applying effective deltas.
|
||||||
*/
|
*/
|
||||||
private readonly transactionRecords = new Map<string, TransactionRecord>();
|
private readonly transactionRecords = new Map<string, TransactionRecord>();
|
||||||
@@ -161,7 +161,7 @@ export class TransactionManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
mergeStoreDeltaSemantics(delta, { txUndoOverrides });
|
mergeStoreDeltaMarkers(delta, { txUndoOverrides });
|
||||||
}
|
}
|
||||||
|
|
||||||
private collectUndoOverrides(delta: StoreDelta): TxUndoOverride[] {
|
private collectUndoOverrides(delta: StoreDelta): TxUndoOverride[] {
|
||||||
@@ -196,7 +196,7 @@ export class TransactionManager {
|
|||||||
delta: HistoryDelta,
|
delta: HistoryDelta,
|
||||||
_context: HistoryEffectiveDeltaResolverContext,
|
_context: HistoryEffectiveDeltaResolverContext,
|
||||||
): HistoryDelta {
|
): HistoryDelta {
|
||||||
const txUndoOverrides = delta.semantics?.txUndoOverrides;
|
const txUndoOverrides = delta.markers?.txUndoOverrides;
|
||||||
if (!txUndoOverrides || txUndoOverrides.length === 0) {
|
if (!txUndoOverrides || txUndoOverrides.length === 0) {
|
||||||
return delta;
|
return delta;
|
||||||
}
|
}
|
||||||
@@ -276,7 +276,7 @@ export class TransactionManager {
|
|||||||
|
|
||||||
return HistoryDelta.create(effectiveElements, delta.appState, {
|
return HistoryDelta.create(effectiveElements, delta.appState, {
|
||||||
id: delta.id,
|
id: delta.id,
|
||||||
semantics: delta.semantics,
|
markers: delta.markers,
|
||||||
}) as HistoryDelta;
|
}) as HistoryDelta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user