alex/auto-undo-redo: add bail-out

alex/no-batches
alex 2024-04-11 16:33:45 +01:00
rodzic 2751a056f2
commit 3448e53a64
7 zmienionych plików z 69 dodań i 41 usunięć

Wyświetl plik

@ -88,25 +88,13 @@ export class SideEffectManager<
return next
}
let updateDepth = 0
editor.store.onAfterChange = (prev, next, source) => {
updateDepth++
if (updateDepth > 1000) {
console.error('[CleanupManager.onAfterChange] Maximum update depth exceeded, bailing out.')
} else {
const handlers = this._afterChangeHandlers[
next.typeName
] as TLAfterChangeHandler<TLRecord>[]
if (handlers) {
for (const handler of handlers) {
handler(prev, next, source)
}
const handlers = this._afterChangeHandlers[next.typeName] as TLAfterChangeHandler<TLRecord>[]
if (handlers) {
for (const handler of handlers) {
handler(prev, next, source)
}
}
updateDepth--
}
editor.store.onBeforeDelete = (record, source) => {

Wyświetl plik

@ -825,38 +825,53 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
this.pendingAfterEvents.set(id, { before, after, source })
}
}
private flushAtomicCallbacks() {
let updateDepth = 0
while (this.pendingAfterEvents) {
const events = this.pendingAfterEvents
this.pendingAfterEvents = null
if (!this._runCallbacks) continue
updateDepth++
if (updateDepth > 100) {
throw new Error('Maximum store update depth exceeded, bailing out')
}
for (const { before, after, source } of events.values()) {
if (before && after) {
this.onAfterChange?.(before, after, source)
} else if (before && !after) {
this.onAfterDelete?.(before, source)
} else if (!before && after) {
this.onAfterCreate?.(after, source)
}
}
}
}
private _isInAtomicOp = false
/** @internal */
atomic<T>(fn: () => T, runCallbacks = true): T {
return transact(() => {
if (this.pendingAfterEvents) return fn()
if (this._isInAtomicOp) {
if (!this.pendingAfterEvents) this.pendingAfterEvents = new Map()
return fn()
}
this.pendingAfterEvents = new Map()
const prevRunCallbacks = this._runCallbacks
this._runCallbacks = runCallbacks ?? prevRunCallbacks
this._isInAtomicOp = true
try {
const result = fn()
while (this.pendingAfterEvents) {
const events = this.pendingAfterEvents
this.pendingAfterEvents = null
if (!runCallbacks) continue
for (const { before, after, source } of events.values()) {
if (before && after) {
this.onAfterChange?.(before, after, source)
} else if (before && !after) {
this.onAfterDelete?.(before, source)
} else if (!before && after) {
this.onAfterCreate?.(after, source)
}
}
}
this.flushAtomicCallbacks()
return result
} finally {
this.pendingAfterEvents = null
this._runCallbacks = prevRunCallbacks
this._isInAtomicOp = false
}
})
}

Wyświetl plik

@ -1157,4 +1157,29 @@ describe('after callbacks', () => {
})
expect(callbacks).toHaveLength(0)
})
it('bails out if too many callbacks are fired', () => {
let limit = 10
store.onAfterCreate = (record) => {
if (record.typeName === 'book' && record.numPages < limit) {
store.put([{ ...record, numPages: record.numPages + 1 }])
}
}
store.onAfterChange = (from, to) => {
if (to.typeName === 'book' && to.numPages < limit) {
store.put([{ ...to, numPages: to.numPages + 1 }])
}
}
// this should be fine:
store.put([Book.create({ title: 'The Hobbit', id: bookId, author: authorId, numPages: 0 })])
expect(store.get(bookId)!.numPages).toBe(limit)
// if we increase the limit thought, it should crash:
limit = 10000
store.clear()
expect(() => {
store.put([Book.create({ title: 'The Hobbit', id: bookId, author: authorId, numPages: 0 })])
}).toThrowErrorMatchingInlineSnapshot(`"Maximum store update depth exceeded, bailing out"`)
})
})

Wyświetl plik

@ -224,9 +224,9 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
isPrecise: boolean;
}>;
point: ObjectValidator< {
type: "point";
x: number;
y: number;
type: "point";
}>;
}, never>;
end: UnionValidator<"type", {
@ -238,9 +238,9 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
isPrecise: boolean;
}>;
point: ObjectValidator< {
type: "point";
x: number;
y: number;
type: "point";
}>;
}, never>;
bend: Validator<number>;

Wyświetl plik

@ -1645,7 +1645,7 @@
},
{
"kind": "Content",
"text": "<{\n type: \"point\";\n x: number;\n y: number;\n }>;\n }, never>;\n end: import(\"@tldraw/editor\")."
"text": "<{\n x: number;\n y: number;\n type: \"point\";\n }>;\n }, never>;\n end: import(\"@tldraw/editor\")."
},
{
"kind": "Reference",
@ -1690,7 +1690,7 @@
},
{
"kind": "Content",
"text": "<{\n type: \"point\";\n x: number;\n y: number;\n }>;\n }, never>;\n bend: import(\"@tldraw/editor\")."
"text": "<{\n x: number;\n y: number;\n type: \"point\";\n }>;\n }, never>;\n bend: import(\"@tldraw/editor\")."
},
{
"kind": "Reference",

Wyświetl plik

@ -47,9 +47,9 @@ export const arrowShapeProps: {
isPrecise: boolean;
} & {}>;
point: T.ObjectValidator<{
type: "point";
x: number;
y: number;
type: "point";
} & {}>;
}, never>;
end: T.UnionValidator<"type", {
@ -61,9 +61,9 @@ export const arrowShapeProps: {
isPrecise: boolean;
} & {}>;
point: T.ObjectValidator<{
type: "point";
x: number;
y: number;
type: "point";
} & {}>;
}, never>;
bend: T.Validator<number>;

Wyświetl plik

@ -364,7 +364,7 @@
},
{
"kind": "Content",
"text": "<{\n type: \"point\";\n x: number;\n y: number;\n } & {}>;\n }, never>;\n end: "
"text": "<{\n x: number;\n y: number;\n type: \"point\";\n } & {}>;\n }, never>;\n end: "
},
{
"kind": "Reference",
@ -409,7 +409,7 @@
},
{
"kind": "Content",
"text": "<{\n type: \"point\";\n x: number;\n y: number;\n } & {}>;\n }, never>;\n bend: "
"text": "<{\n x: number;\n y: number;\n type: \"point\";\n } & {}>;\n }, never>;\n bend: "
},
{
"kind": "Reference",