This commit is contained in:
440
frontend/node_modules/immer/src/plugins/arrayMethods.ts
generated
vendored
Normal file
440
frontend/node_modules/immer/src/plugins/arrayMethods.ts
generated
vendored
Normal file
@@ -0,0 +1,440 @@
|
||||
import {
|
||||
PluginArrayMethods,
|
||||
latest,
|
||||
loadPlugin,
|
||||
markChanged,
|
||||
prepareCopy,
|
||||
handleCrossReference,
|
||||
ProxyArrayState
|
||||
} from "../internal"
|
||||
|
||||
/**
|
||||
* Methods that directly modify the array in place.
|
||||
* These operate on the copy without creating per-element proxies:
|
||||
* - `push`, `pop`: Add/remove from end
|
||||
* - `shift`, `unshift`: Add/remove from start (marks all indices reassigned)
|
||||
* - `splice`: Add/remove at arbitrary position (marks all indices reassigned)
|
||||
* - `reverse`, `sort`: Reorder elements (marks all indices reassigned)
|
||||
*/
|
||||
type MutatingArrayMethod =
|
||||
| "push"
|
||||
| "pop"
|
||||
| "shift"
|
||||
| "unshift"
|
||||
| "splice"
|
||||
| "reverse"
|
||||
| "sort"
|
||||
|
||||
/**
|
||||
* Methods that read from the array without modifying it.
|
||||
* These fall into distinct categories based on return semantics:
|
||||
*
|
||||
* **Subset operations** (return drafts - mutations propagate):
|
||||
* - `filter`, `slice`: Return array of draft proxies
|
||||
* - `find`, `findLast`: Return single draft proxy or undefined
|
||||
*
|
||||
* **Transform operations** (return base values - mutations don't track):
|
||||
* - `concat`, `flat`: Create new structures, not subsets of original
|
||||
*
|
||||
* **Primitive-returning** (no draft needed):
|
||||
* - `findIndex`, `findLastIndex`, `indexOf`, `lastIndexOf`: Return numbers
|
||||
* - `some`, `every`, `includes`: Return booleans
|
||||
* - `join`, `toString`, `toLocaleString`: Return strings
|
||||
*/
|
||||
type NonMutatingArrayMethod =
|
||||
| "filter"
|
||||
| "slice"
|
||||
| "concat"
|
||||
| "flat"
|
||||
| "find"
|
||||
| "findIndex"
|
||||
| "findLast"
|
||||
| "findLastIndex"
|
||||
| "some"
|
||||
| "every"
|
||||
| "indexOf"
|
||||
| "lastIndexOf"
|
||||
| "includes"
|
||||
| "join"
|
||||
| "toString"
|
||||
| "toLocaleString"
|
||||
|
||||
/** Union of all array operation methods handled by the plugin. */
|
||||
export type ArrayOperationMethod = MutatingArrayMethod | NonMutatingArrayMethod
|
||||
|
||||
/**
|
||||
* Enables optimized array method handling for Immer drafts.
|
||||
*
|
||||
* This plugin overrides array methods to avoid unnecessary Proxy creation during iteration,
|
||||
* significantly improving performance for array-heavy operations.
|
||||
*
|
||||
* **Mutating methods** (push, pop, shift, unshift, splice, sort, reverse):
|
||||
* Operate directly on the copy without creating per-element proxies.
|
||||
*
|
||||
* **Non-mutating methods** fall into categories:
|
||||
* - **Subset operations** (filter, slice, find, findLast): Return draft proxies - mutations track
|
||||
* - **Transform operations** (concat, flat): Return base values - mutations don't track
|
||||
* - **Primitive-returning** (indexOf, includes, some, every, etc.): Return primitives
|
||||
*
|
||||
* **Important**: Callbacks for overridden methods receive base values, not drafts.
|
||||
* This is the core performance optimization.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { enableArrayMethods, produce } from "immer"
|
||||
*
|
||||
* enableArrayMethods()
|
||||
*
|
||||
* const next = produce(state, draft => {
|
||||
* // Optimized - no proxy creation per element
|
||||
* draft.items.sort((a, b) => a.value - b.value)
|
||||
*
|
||||
* // filter returns drafts - mutations propagate
|
||||
* const filtered = draft.items.filter(x => x.value > 5)
|
||||
* filtered[0].value = 999 // Affects draft.items[originalIndex]
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @see https://immerjs.github.io/immer/array-methods
|
||||
*/
|
||||
export function enableArrayMethods() {
|
||||
const SHIFTING_METHODS = new Set<MutatingArrayMethod>(["shift", "unshift"])
|
||||
|
||||
const QUEUE_METHODS = new Set<MutatingArrayMethod>(["push", "pop"])
|
||||
|
||||
const RESULT_RETURNING_METHODS = new Set<MutatingArrayMethod>([
|
||||
...QUEUE_METHODS,
|
||||
...SHIFTING_METHODS
|
||||
])
|
||||
|
||||
const REORDERING_METHODS = new Set<MutatingArrayMethod>(["reverse", "sort"])
|
||||
|
||||
// Optimized method detection using array-based lookup
|
||||
const MUTATING_METHODS = new Set<MutatingArrayMethod>([
|
||||
...RESULT_RETURNING_METHODS,
|
||||
...REORDERING_METHODS,
|
||||
"splice"
|
||||
])
|
||||
|
||||
const FIND_METHODS = new Set<NonMutatingArrayMethod>(["find", "findLast"])
|
||||
|
||||
const NON_MUTATING_METHODS = new Set<NonMutatingArrayMethod>([
|
||||
"filter",
|
||||
"slice",
|
||||
"concat",
|
||||
"flat",
|
||||
...FIND_METHODS,
|
||||
"findIndex",
|
||||
"findLastIndex",
|
||||
"some",
|
||||
"every",
|
||||
"indexOf",
|
||||
"lastIndexOf",
|
||||
"includes",
|
||||
"join",
|
||||
"toString",
|
||||
"toLocaleString"
|
||||
])
|
||||
|
||||
// Type guard for method detection
|
||||
function isMutatingArrayMethod(
|
||||
method: string
|
||||
): method is MutatingArrayMethod {
|
||||
return MUTATING_METHODS.has(method as any)
|
||||
}
|
||||
|
||||
function isNonMutatingArrayMethod(
|
||||
method: string
|
||||
): method is NonMutatingArrayMethod {
|
||||
return NON_MUTATING_METHODS.has(method as any)
|
||||
}
|
||||
|
||||
function isArrayOperationMethod(
|
||||
method: string
|
||||
): method is ArrayOperationMethod {
|
||||
return isMutatingArrayMethod(method) || isNonMutatingArrayMethod(method)
|
||||
}
|
||||
|
||||
function enterOperation(
|
||||
state: ProxyArrayState,
|
||||
method: ArrayOperationMethod
|
||||
) {
|
||||
state.operationMethod = method
|
||||
}
|
||||
|
||||
function exitOperation(state: ProxyArrayState) {
|
||||
state.operationMethod = undefined
|
||||
}
|
||||
|
||||
// Shared utility functions for array method handlers
|
||||
function executeArrayMethod<T>(
|
||||
state: ProxyArrayState,
|
||||
operation: () => T,
|
||||
markLength = true
|
||||
): T {
|
||||
prepareCopy(state)
|
||||
const result = operation()
|
||||
markChanged(state)
|
||||
if (markLength) state.assigned_!.set("length", true)
|
||||
return result
|
||||
}
|
||||
|
||||
function markAllIndicesReassigned(state: ProxyArrayState) {
|
||||
state.allIndicesReassigned_ = true
|
||||
}
|
||||
|
||||
function normalizeSliceIndex(index: number, length: number): number {
|
||||
if (index < 0) {
|
||||
return Math.max(length + index, 0)
|
||||
}
|
||||
return Math.min(index, length)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls handleCrossReference for each value being inserted into the array,
|
||||
* and marks the corresponding indices as assigned in `assigned_`.
|
||||
*
|
||||
* This ensures nested drafts inside inserted values (e.g. from spreading
|
||||
* a draft object) are properly finalized, matching the behavior of the
|
||||
* proxy set trap which calls handleCrossReference on every assignment.
|
||||
*
|
||||
* Without this, values containing draft proxies (like `{...state[0]}`)
|
||||
* pushed via the array methods plugin would have their nested drafts
|
||||
* revoked during finalization without being replaced by final values.
|
||||
*/
|
||||
function handleInsertedValues(
|
||||
state: ProxyArrayState,
|
||||
startIndex: number,
|
||||
values: any[]
|
||||
) {
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
const index = startIndex + i
|
||||
state.assigned_!.set(index, true)
|
||||
handleCrossReference(state, index, values[i])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mutating operations that add/remove elements (push, pop, shift, unshift, splice).
|
||||
*
|
||||
* Operates directly on `state.copy_` without creating per-element proxies.
|
||||
* For shifting methods (shift, unshift), marks all indices as reassigned since
|
||||
* indices shift.
|
||||
*
|
||||
* @returns For push/pop/shift/unshift: the native method result. For others: the draft.
|
||||
*/
|
||||
function handleSimpleOperation(
|
||||
state: ProxyArrayState,
|
||||
method: string,
|
||||
args: any[]
|
||||
) {
|
||||
return executeArrayMethod(state, () => {
|
||||
// For push/unshift, capture the length before the operation
|
||||
// so we can compute insertion indices for handleCrossReference
|
||||
const lengthBefore = state.copy_!.length
|
||||
|
||||
const result = (state.copy_! as any)[method](...args)
|
||||
|
||||
// Handle index reassignment for shifting methods
|
||||
if (SHIFTING_METHODS.has(method as MutatingArrayMethod)) {
|
||||
markAllIndicesReassigned(state)
|
||||
}
|
||||
|
||||
// Handle cross-references for newly inserted values.
|
||||
// push appends at the end, unshift inserts at the beginning.
|
||||
if (method === "push" && args.length > 0) {
|
||||
handleInsertedValues(state, lengthBefore, args)
|
||||
} else if (method === "unshift" && args.length > 0) {
|
||||
handleInsertedValues(state, 0, args)
|
||||
}
|
||||
|
||||
// Return appropriate value based on method
|
||||
return RESULT_RETURNING_METHODS.has(method as MutatingArrayMethod)
|
||||
? result
|
||||
: state.draft_
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles reordering operations (reverse, sort) that change element order.
|
||||
*
|
||||
* Operates directly on `state.copy_` and marks all indices as reassigned
|
||||
* since element positions change. Does not mark length as changed since
|
||||
* these operations preserve array length.
|
||||
*
|
||||
* @returns The draft proxy for method chaining.
|
||||
*/
|
||||
function handleReorderingOperation(
|
||||
state: ProxyArrayState,
|
||||
method: string,
|
||||
args: any[]
|
||||
) {
|
||||
return executeArrayMethod(
|
||||
state,
|
||||
() => {
|
||||
;(state.copy_! as any)[method](...args)
|
||||
markAllIndicesReassigned(state)
|
||||
return state.draft_
|
||||
},
|
||||
false
|
||||
) // Don't mark length as changed
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an interceptor function for a specific array method.
|
||||
*
|
||||
* The interceptor wraps array method calls to:
|
||||
* 1. Set `state.operationMethod` flag during execution (allows proxy `get` trap
|
||||
* to detect we're inside an optimized method and skip proxy creation)
|
||||
* 2. Route to appropriate handler based on method type
|
||||
* 3. Clean up the operation flag in `finally` block
|
||||
*
|
||||
* The `operationMethod` flag is the key mechanism that enables the proxy's `get`
|
||||
* trap to return base values instead of creating nested proxies during iteration.
|
||||
*
|
||||
* @param state - The proxy array state
|
||||
* @param originalMethod - Name of the array method being intercepted
|
||||
* @returns Interceptor function that handles the method call
|
||||
*/
|
||||
function createMethodInterceptor(
|
||||
state: ProxyArrayState,
|
||||
originalMethod: string
|
||||
) {
|
||||
return function interceptedMethod(...args: any[]) {
|
||||
// Enter operation mode - this flag tells the proxy's get trap to return
|
||||
// base values instead of creating nested proxies during iteration
|
||||
const method = originalMethod as ArrayOperationMethod
|
||||
enterOperation(state, method)
|
||||
|
||||
try {
|
||||
// Check if this is a mutating method
|
||||
if (isMutatingArrayMethod(method)) {
|
||||
// Direct method dispatch - no configuration lookup needed
|
||||
if (RESULT_RETURNING_METHODS.has(method)) {
|
||||
return handleSimpleOperation(state, method, args)
|
||||
}
|
||||
if (REORDERING_METHODS.has(method)) {
|
||||
return handleReorderingOperation(state, method, args)
|
||||
}
|
||||
|
||||
if (method === "splice") {
|
||||
const res = executeArrayMethod(state, () =>
|
||||
state.copy_!.splice(...(args as [number, number, ...any[]]))
|
||||
)
|
||||
markAllIndicesReassigned(state)
|
||||
// Handle cross-references for inserted values (args from index 2+)
|
||||
if (args.length > 2) {
|
||||
const startIndex = normalizeSliceIndex(
|
||||
args[0] ?? 0,
|
||||
state.copy_!.length
|
||||
)
|
||||
handleInsertedValues(state, startIndex, args.slice(2))
|
||||
}
|
||||
return res
|
||||
}
|
||||
} else {
|
||||
// Handle non-mutating methods
|
||||
return handleNonMutatingOperation(state, method, args)
|
||||
}
|
||||
} finally {
|
||||
// Always exit operation mode - must be in finally to handle exceptions
|
||||
exitOperation(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles non-mutating array methods with different return semantics.
|
||||
*
|
||||
* **Subset operations** return draft proxies for mutation tracking:
|
||||
* - `filter`, `slice`: Return `state.draft_[i]` for each selected element
|
||||
* - `find`, `findLast`: Return `state.draft_[i]` for the found element
|
||||
*
|
||||
* This allows mutations on returned elements to propagate back to the draft:
|
||||
* ```ts
|
||||
* const filtered = draft.items.filter(x => x.value > 5)
|
||||
* filtered[0].value = 999 // Mutates draft.items[originalIndex]
|
||||
* ```
|
||||
*
|
||||
* **Transform operations** return base values (no draft tracking):
|
||||
* - `concat`, `flat`: These create NEW arrays rather than selecting subsets.
|
||||
* Since the result structure differs from the original, tracking mutations
|
||||
* back to specific draft indices would be impractical/impossible.
|
||||
*
|
||||
* **Primitive operations** return the native result directly:
|
||||
* - `indexOf`, `includes`, `some`, `every`, `join`, etc.
|
||||
*
|
||||
* @param state - The proxy array state
|
||||
* @param method - The non-mutating method name
|
||||
* @param args - Arguments passed to the method
|
||||
* @returns Drafts for subset operations, base values for transforms, primitives otherwise
|
||||
*/
|
||||
function handleNonMutatingOperation(
|
||||
state: ProxyArrayState,
|
||||
method: NonMutatingArrayMethod,
|
||||
args: any[]
|
||||
) {
|
||||
const source = latest(state)
|
||||
|
||||
// Methods that return arrays with selected items - need to return drafts
|
||||
if (method === "filter") {
|
||||
const predicate = args[0]
|
||||
const result: any[] = []
|
||||
|
||||
// First pass: call predicate on base values to determine which items pass
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
if (predicate(source[i], i, source)) {
|
||||
// Only create draft for items that passed the predicate
|
||||
result.push(state.draft_[i])
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
if (FIND_METHODS.has(method)) {
|
||||
const predicate = args[0]
|
||||
const isForward = method === "find"
|
||||
const step = isForward ? 1 : -1
|
||||
const start = isForward ? 0 : source.length - 1
|
||||
|
||||
for (let i = start; i >= 0 && i < source.length; i += step) {
|
||||
if (predicate(source[i], i, source)) {
|
||||
return state.draft_[i]
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (method === "slice") {
|
||||
const rawStart = args[0] ?? 0
|
||||
const rawEnd = args[1] ?? source.length
|
||||
|
||||
// Normalize negative indices
|
||||
const start = normalizeSliceIndex(rawStart, source.length)
|
||||
const end = normalizeSliceIndex(rawEnd, source.length)
|
||||
|
||||
const result: any[] = []
|
||||
|
||||
// Return drafts for items in the slice range
|
||||
for (let i = start; i < end; i++) {
|
||||
result.push(state.draft_[i])
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// For other methods, call on base array directly:
|
||||
// - indexOf, includes, join, toString: Return primitives, no draft needed
|
||||
// - concat, flat: Return NEW arrays (not subsets). Elements are base values.
|
||||
// This is intentional - concat/flat create new data structures rather than
|
||||
// selecting subsets of the original, making draft tracking impractical.
|
||||
return source[method as keyof typeof Array.prototype](...args)
|
||||
}
|
||||
|
||||
loadPlugin(PluginArrayMethods, {
|
||||
createMethodInterceptor,
|
||||
isArrayOperationMethod,
|
||||
isMutatingArrayMethod
|
||||
})
|
||||
}
|
||||
332
frontend/node_modules/immer/src/plugins/mapset.ts
generated
vendored
Normal file
332
frontend/node_modules/immer/src/plugins/mapset.ts
generated
vendored
Normal file
@@ -0,0 +1,332 @@
|
||||
// types only!
|
||||
import {
|
||||
ImmerState,
|
||||
AnyMap,
|
||||
AnySet,
|
||||
MapState,
|
||||
SetState,
|
||||
DRAFT_STATE,
|
||||
getCurrentScope,
|
||||
latest,
|
||||
isDraftable,
|
||||
createProxy,
|
||||
loadPlugin,
|
||||
markChanged,
|
||||
die,
|
||||
ArchType,
|
||||
each,
|
||||
getValue,
|
||||
PluginMapSet,
|
||||
handleCrossReference
|
||||
} from "../internal"
|
||||
|
||||
export function enableMapSet() {
|
||||
class DraftMap extends Map {
|
||||
[DRAFT_STATE]: MapState
|
||||
|
||||
constructor(target: AnyMap, parent?: ImmerState) {
|
||||
super()
|
||||
this[DRAFT_STATE] = {
|
||||
type_: ArchType.Map,
|
||||
parent_: parent,
|
||||
scope_: parent ? parent.scope_ : getCurrentScope()!,
|
||||
modified_: false,
|
||||
finalized_: false,
|
||||
copy_: undefined,
|
||||
assigned_: undefined,
|
||||
base_: target,
|
||||
draft_: this as any,
|
||||
isManual_: false,
|
||||
revoked_: false,
|
||||
callbacks_: []
|
||||
}
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return latest(this[DRAFT_STATE]).size
|
||||
}
|
||||
|
||||
has(key: any): boolean {
|
||||
return latest(this[DRAFT_STATE]).has(key)
|
||||
}
|
||||
|
||||
set(key: any, value: any) {
|
||||
const state: MapState = this[DRAFT_STATE]
|
||||
assertUnrevoked(state)
|
||||
if (!latest(state).has(key) || latest(state).get(key) !== value) {
|
||||
prepareMapCopy(state)
|
||||
markChanged(state)
|
||||
state.assigned_!.set(key, true)
|
||||
state.copy_!.set(key, value)
|
||||
state.assigned_!.set(key, true)
|
||||
handleCrossReference(state, key, value)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
delete(key: any): boolean {
|
||||
if (!this.has(key)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const state: MapState = this[DRAFT_STATE]
|
||||
assertUnrevoked(state)
|
||||
prepareMapCopy(state)
|
||||
markChanged(state)
|
||||
if (state.base_.has(key)) {
|
||||
state.assigned_!.set(key, false)
|
||||
} else {
|
||||
state.assigned_!.delete(key)
|
||||
}
|
||||
state.copy_!.delete(key)
|
||||
return true
|
||||
}
|
||||
|
||||
clear() {
|
||||
const state: MapState = this[DRAFT_STATE]
|
||||
assertUnrevoked(state)
|
||||
if (latest(state).size) {
|
||||
prepareMapCopy(state)
|
||||
markChanged(state)
|
||||
state.assigned_ = new Map()
|
||||
each(state.base_, key => {
|
||||
state.assigned_!.set(key, false)
|
||||
})
|
||||
state.copy_!.clear()
|
||||
}
|
||||
}
|
||||
|
||||
forEach(cb: (value: any, key: any, self: any) => void, thisArg?: any) {
|
||||
const state: MapState = this[DRAFT_STATE]
|
||||
latest(state).forEach((_value: any, key: any, _map: any) => {
|
||||
cb.call(thisArg, this.get(key), key, this)
|
||||
})
|
||||
}
|
||||
|
||||
get(key: any): any {
|
||||
const state: MapState = this[DRAFT_STATE]
|
||||
assertUnrevoked(state)
|
||||
const value = latest(state).get(key)
|
||||
if (state.finalized_ || !isDraftable(value)) {
|
||||
return value
|
||||
}
|
||||
if (value !== state.base_.get(key)) {
|
||||
return value // either already drafted or reassigned
|
||||
}
|
||||
// despite what it looks, this creates a draft only once, see above condition
|
||||
const draft = createProxy(state.scope_, value, state, key)
|
||||
prepareMapCopy(state)
|
||||
state.copy_!.set(key, draft)
|
||||
return draft
|
||||
}
|
||||
|
||||
keys(): IterableIterator<any> {
|
||||
return latest(this[DRAFT_STATE]).keys()
|
||||
}
|
||||
|
||||
values(): IterableIterator<any> {
|
||||
const iterator = this.keys()
|
||||
return {
|
||||
[Symbol.iterator]: () => this.values(),
|
||||
next: () => {
|
||||
const r = iterator.next()
|
||||
/* istanbul ignore next */
|
||||
if (r.done) return r
|
||||
const value = this.get(r.value)
|
||||
return {
|
||||
done: false,
|
||||
value
|
||||
}
|
||||
}
|
||||
} as any
|
||||
}
|
||||
|
||||
entries(): IterableIterator<[any, any]> {
|
||||
const iterator = this.keys()
|
||||
return {
|
||||
[Symbol.iterator]: () => this.entries(),
|
||||
next: () => {
|
||||
const r = iterator.next()
|
||||
/* istanbul ignore next */
|
||||
if (r.done) return r
|
||||
const value = this.get(r.value)
|
||||
return {
|
||||
done: false,
|
||||
value: [r.value, value]
|
||||
}
|
||||
}
|
||||
} as any
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return this.entries()
|
||||
}
|
||||
}
|
||||
|
||||
function proxyMap_<T extends AnyMap>(
|
||||
target: T,
|
||||
parent?: ImmerState
|
||||
): [T, MapState] {
|
||||
// @ts-ignore
|
||||
const map = new DraftMap(target, parent)
|
||||
return [map as any, map[DRAFT_STATE]]
|
||||
}
|
||||
|
||||
function prepareMapCopy(state: MapState) {
|
||||
if (!state.copy_) {
|
||||
state.assigned_ = new Map()
|
||||
state.copy_ = new Map(state.base_)
|
||||
}
|
||||
}
|
||||
|
||||
class DraftSet extends Set {
|
||||
[DRAFT_STATE]: SetState
|
||||
constructor(target: AnySet, parent?: ImmerState) {
|
||||
super()
|
||||
this[DRAFT_STATE] = {
|
||||
type_: ArchType.Set,
|
||||
parent_: parent,
|
||||
scope_: parent ? parent.scope_ : getCurrentScope()!,
|
||||
modified_: false,
|
||||
finalized_: false,
|
||||
copy_: undefined,
|
||||
base_: target,
|
||||
draft_: this,
|
||||
drafts_: new Map(),
|
||||
revoked_: false,
|
||||
isManual_: false,
|
||||
assigned_: undefined,
|
||||
callbacks_: []
|
||||
}
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return latest(this[DRAFT_STATE]).size
|
||||
}
|
||||
|
||||
has(value: any): boolean {
|
||||
const state: SetState = this[DRAFT_STATE]
|
||||
assertUnrevoked(state)
|
||||
// bit of trickery here, to be able to recognize both the value, and the draft of its value
|
||||
if (!state.copy_) {
|
||||
return state.base_.has(value)
|
||||
}
|
||||
if (state.copy_.has(value)) return true
|
||||
if (state.drafts_.has(value) && state.copy_.has(state.drafts_.get(value)))
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
add(value: any): any {
|
||||
const state: SetState = this[DRAFT_STATE]
|
||||
assertUnrevoked(state)
|
||||
if (!this.has(value)) {
|
||||
prepareSetCopy(state)
|
||||
markChanged(state)
|
||||
state.copy_!.add(value)
|
||||
handleCrossReference(state, value, value)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
delete(value: any): any {
|
||||
if (!this.has(value)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const state: SetState = this[DRAFT_STATE]
|
||||
assertUnrevoked(state)
|
||||
prepareSetCopy(state)
|
||||
markChanged(state)
|
||||
return (
|
||||
state.copy_!.delete(value) ||
|
||||
(state.drafts_.has(value)
|
||||
? state.copy_!.delete(state.drafts_.get(value))
|
||||
: /* istanbul ignore next */ false)
|
||||
)
|
||||
}
|
||||
|
||||
clear() {
|
||||
const state: SetState = this[DRAFT_STATE]
|
||||
assertUnrevoked(state)
|
||||
if (latest(state).size) {
|
||||
prepareSetCopy(state)
|
||||
markChanged(state)
|
||||
state.copy_!.clear()
|
||||
}
|
||||
}
|
||||
|
||||
values(): IterableIterator<any> {
|
||||
const state: SetState = this[DRAFT_STATE]
|
||||
assertUnrevoked(state)
|
||||
prepareSetCopy(state)
|
||||
return state.copy_!.values()
|
||||
}
|
||||
|
||||
entries(): IterableIterator<[any, any]> {
|
||||
const state: SetState = this[DRAFT_STATE]
|
||||
assertUnrevoked(state)
|
||||
prepareSetCopy(state)
|
||||
return state.copy_!.entries()
|
||||
}
|
||||
|
||||
keys(): IterableIterator<any> {
|
||||
return this.values()
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return this.values()
|
||||
}
|
||||
|
||||
forEach(cb: any, thisArg?: any) {
|
||||
const iterator = this.values()
|
||||
let result = iterator.next()
|
||||
while (!result.done) {
|
||||
cb.call(thisArg, result.value, result.value, this)
|
||||
result = iterator.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
function proxySet_<T extends AnySet>(
|
||||
target: T,
|
||||
parent?: ImmerState
|
||||
): [T, SetState] {
|
||||
// @ts-ignore
|
||||
const set = new DraftSet(target, parent)
|
||||
return [set as any, set[DRAFT_STATE]]
|
||||
}
|
||||
|
||||
function prepareSetCopy(state: SetState) {
|
||||
if (!state.copy_) {
|
||||
// create drafts for all entries to preserve insertion order
|
||||
state.copy_ = new Set()
|
||||
state.base_.forEach(value => {
|
||||
if (isDraftable(value)) {
|
||||
const draft = createProxy(state.scope_, value, state, value)
|
||||
state.drafts_.set(value, draft)
|
||||
state.copy_!.add(draft)
|
||||
} else {
|
||||
state.copy_!.add(value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function assertUnrevoked(state: any /*ES5State | MapState | SetState*/) {
|
||||
if (state.revoked_) die(3, JSON.stringify(latest(state)))
|
||||
}
|
||||
|
||||
function fixSetContents(target: ImmerState) {
|
||||
// For sets we clone before iterating, otherwise we can get in endless loop due to modifying during iteration, see #628
|
||||
// To preserve insertion order in all cases we then clear the set
|
||||
if (target.type_ === ArchType.Set && target.copy_) {
|
||||
const copy = new Set(target.copy_)
|
||||
target.copy_.clear()
|
||||
copy.forEach(value => {
|
||||
target.copy_!.add(getValue(value))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
loadPlugin(PluginMapSet, {proxyMap_, proxySet_, fixSetContents})
|
||||
}
|
||||
431
frontend/node_modules/immer/src/plugins/patches.ts
generated
vendored
Normal file
431
frontend/node_modules/immer/src/plugins/patches.ts
generated
vendored
Normal file
@@ -0,0 +1,431 @@
|
||||
import {immerable} from "../immer"
|
||||
import {
|
||||
ImmerState,
|
||||
Patch,
|
||||
SetState,
|
||||
ProxyArrayState,
|
||||
MapState,
|
||||
ProxyObjectState,
|
||||
PatchPath,
|
||||
get,
|
||||
each,
|
||||
has,
|
||||
getArchtype,
|
||||
getPrototypeOf,
|
||||
isSet,
|
||||
isMap,
|
||||
loadPlugin,
|
||||
ArchType,
|
||||
die,
|
||||
isDraft,
|
||||
isDraftable,
|
||||
NOTHING,
|
||||
errors,
|
||||
DRAFT_STATE,
|
||||
getProxyDraft,
|
||||
ImmerScope,
|
||||
isObjectish,
|
||||
isFunction,
|
||||
CONSTRUCTOR,
|
||||
PluginPatches,
|
||||
isArray,
|
||||
PROTOTYPE
|
||||
} from "../internal"
|
||||
|
||||
export function enablePatches() {
|
||||
const errorOffset = 16
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
errors.push(
|
||||
'Sets cannot have "replace" patches.',
|
||||
function(op: string) {
|
||||
return "Unsupported patch operation: " + op
|
||||
},
|
||||
function(path: string) {
|
||||
return "Cannot apply patch, path doesn't resolve: " + path
|
||||
},
|
||||
"Patching reserved attributes like __proto__, prototype and constructor is not allowed"
|
||||
)
|
||||
}
|
||||
|
||||
function getPath(state: ImmerState, path: PatchPath = []): PatchPath | null {
|
||||
// Step 1: Check if state has a stored key
|
||||
if (state.key_ !== undefined) {
|
||||
// Step 2: Validate the key is still valid in parent
|
||||
|
||||
const parentCopy = state.parent_!.copy_ ?? state.parent_!.base_
|
||||
const proxyDraft = getProxyDraft(get(parentCopy, state.key_!))
|
||||
const valueAtKey = get(parentCopy, state.key_!)
|
||||
|
||||
if (valueAtKey === undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Check if the value at the key is still related to this draft
|
||||
// It should be either the draft itself, the base, or the copy
|
||||
if (
|
||||
valueAtKey !== state.draft_ &&
|
||||
valueAtKey !== state.base_ &&
|
||||
valueAtKey !== state.copy_
|
||||
) {
|
||||
return null // Value was replaced with something else
|
||||
}
|
||||
if (proxyDraft != null && proxyDraft.base_ !== state.base_) {
|
||||
return null // Different draft
|
||||
}
|
||||
|
||||
// Step 3: Handle Set case specially
|
||||
const isSet = state.parent_!.type_ === ArchType.Set
|
||||
let key: string | number
|
||||
|
||||
if (isSet) {
|
||||
// For Sets, find the index in the drafts_ map
|
||||
const setParent = state.parent_ as SetState
|
||||
key = Array.from(setParent.drafts_.keys()).indexOf(state.key_)
|
||||
} else {
|
||||
key = state.key_ as string | number
|
||||
}
|
||||
|
||||
// Step 4: Validate key still exists in parent
|
||||
if (!((isSet && parentCopy.size > key) || has(parentCopy, key))) {
|
||||
return null // Key deleted
|
||||
}
|
||||
|
||||
// Step 5: Add key to path
|
||||
path.push(key)
|
||||
}
|
||||
|
||||
// Step 6: Recurse to parent if exists
|
||||
if (state.parent_) {
|
||||
return getPath(state.parent_, path)
|
||||
}
|
||||
|
||||
// Step 7: At root - reverse path and validate
|
||||
path.reverse()
|
||||
|
||||
try {
|
||||
// Validate path can be resolved from ROOT
|
||||
resolvePath(state.copy_, path)
|
||||
} catch (e) {
|
||||
return null // Path invalid
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
// NEW: Add resolvePath helper function
|
||||
function resolvePath(base: any, path: PatchPath): any {
|
||||
let current = base
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
const key = path[i]
|
||||
current = get(current, key)
|
||||
if (!isObjectish(current) || current === null) {
|
||||
throw new Error(`Cannot resolve path at '${path.join("/")}'`)
|
||||
}
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
const REPLACE = "replace"
|
||||
const ADD = "add"
|
||||
const REMOVE = "remove"
|
||||
|
||||
function generatePatches_(
|
||||
state: ImmerState,
|
||||
basePath: PatchPath,
|
||||
scope: ImmerScope
|
||||
): void {
|
||||
if (state.scope_.processedForPatches_.has(state)) {
|
||||
return
|
||||
}
|
||||
|
||||
state.scope_.processedForPatches_.add(state)
|
||||
|
||||
const {patches_, inversePatches_} = scope
|
||||
|
||||
switch (state.type_) {
|
||||
case ArchType.Object:
|
||||
case ArchType.Map:
|
||||
return generatePatchesFromAssigned(
|
||||
state,
|
||||
basePath,
|
||||
patches_!,
|
||||
inversePatches_!
|
||||
)
|
||||
case ArchType.Array:
|
||||
return generateArrayPatches(
|
||||
state,
|
||||
basePath,
|
||||
patches_!,
|
||||
inversePatches_!
|
||||
)
|
||||
case ArchType.Set:
|
||||
return generateSetPatches(
|
||||
(state as any) as SetState,
|
||||
basePath,
|
||||
patches_!,
|
||||
inversePatches_!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function generateArrayPatches(
|
||||
state: ProxyArrayState,
|
||||
basePath: PatchPath,
|
||||
patches: Patch[],
|
||||
inversePatches: Patch[]
|
||||
) {
|
||||
let {base_, assigned_} = state
|
||||
let copy_ = state.copy_!
|
||||
|
||||
// Reduce complexity by ensuring `base` is never longer.
|
||||
if (copy_.length < base_.length) {
|
||||
// @ts-ignore
|
||||
;[base_, copy_] = [copy_, base_]
|
||||
;[patches, inversePatches] = [inversePatches, patches]
|
||||
}
|
||||
|
||||
const allReassigned = state.allIndicesReassigned_ === true
|
||||
|
||||
// Process replaced indices.
|
||||
for (let i = 0; i < base_.length; i++) {
|
||||
const copiedItem = copy_[i]
|
||||
const baseItem = base_[i]
|
||||
|
||||
const isAssigned = allReassigned || assigned_?.get(i.toString())
|
||||
if (isAssigned && copiedItem !== baseItem) {
|
||||
const childState = copiedItem?.[DRAFT_STATE]
|
||||
if (childState && childState.modified_) {
|
||||
// Skip - let the child generate its own patches
|
||||
continue
|
||||
}
|
||||
const path = basePath.concat([i])
|
||||
patches.push({
|
||||
op: REPLACE,
|
||||
path,
|
||||
// Need to maybe clone it, as it can in fact be the original value
|
||||
// due to the base/copy inversion at the start of this function
|
||||
value: clonePatchValueIfNeeded(copiedItem)
|
||||
})
|
||||
inversePatches.push({
|
||||
op: REPLACE,
|
||||
path,
|
||||
value: clonePatchValueIfNeeded(baseItem)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Process added indices.
|
||||
for (let i = base_.length; i < copy_.length; i++) {
|
||||
const path = basePath.concat([i])
|
||||
patches.push({
|
||||
op: ADD,
|
||||
path,
|
||||
// Need to maybe clone it, as it can in fact be the original value
|
||||
// due to the base/copy inversion at the start of this function
|
||||
value: clonePatchValueIfNeeded(copy_[i])
|
||||
})
|
||||
}
|
||||
for (let i = copy_.length - 1; base_.length <= i; --i) {
|
||||
const path = basePath.concat([i])
|
||||
inversePatches.push({
|
||||
op: REMOVE,
|
||||
path
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// This is used for both Map objects and normal objects.
|
||||
function generatePatchesFromAssigned(
|
||||
state: MapState | ProxyObjectState,
|
||||
basePath: PatchPath,
|
||||
patches: Patch[],
|
||||
inversePatches: Patch[]
|
||||
) {
|
||||
const {base_, copy_, type_} = state
|
||||
each(state.assigned_!, (key, assignedValue) => {
|
||||
const origValue = get(base_, key, type_)
|
||||
const value = get(copy_!, key, type_)
|
||||
const op = !assignedValue ? REMOVE : has(base_, key) ? REPLACE : ADD
|
||||
if (origValue === value && op === REPLACE) return
|
||||
const path = basePath.concat(key as any)
|
||||
patches.push(
|
||||
op === REMOVE
|
||||
? {op, path}
|
||||
: {op, path, value: clonePatchValueIfNeeded(value)}
|
||||
)
|
||||
inversePatches.push(
|
||||
op === ADD
|
||||
? {op: REMOVE, path}
|
||||
: op === REMOVE
|
||||
? {op: ADD, path, value: clonePatchValueIfNeeded(origValue)}
|
||||
: {op: REPLACE, path, value: clonePatchValueIfNeeded(origValue)}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function generateSetPatches(
|
||||
state: SetState,
|
||||
basePath: PatchPath,
|
||||
patches: Patch[],
|
||||
inversePatches: Patch[]
|
||||
) {
|
||||
let {base_, copy_} = state
|
||||
|
||||
let i = 0
|
||||
base_.forEach((value: any) => {
|
||||
if (!copy_!.has(value)) {
|
||||
const path = basePath.concat([i])
|
||||
patches.push({
|
||||
op: REMOVE,
|
||||
path,
|
||||
value
|
||||
})
|
||||
inversePatches.unshift({
|
||||
op: ADD,
|
||||
path,
|
||||
value
|
||||
})
|
||||
}
|
||||
i++
|
||||
})
|
||||
i = 0
|
||||
copy_!.forEach((value: any) => {
|
||||
if (!base_.has(value)) {
|
||||
const path = basePath.concat([i])
|
||||
patches.push({
|
||||
op: ADD,
|
||||
path,
|
||||
value
|
||||
})
|
||||
inversePatches.unshift({
|
||||
op: REMOVE,
|
||||
path,
|
||||
value
|
||||
})
|
||||
}
|
||||
i++
|
||||
})
|
||||
}
|
||||
|
||||
function generateReplacementPatches_(
|
||||
baseValue: any,
|
||||
replacement: any,
|
||||
scope: ImmerScope
|
||||
): void {
|
||||
const {patches_, inversePatches_} = scope
|
||||
patches_!.push({
|
||||
op: REPLACE,
|
||||
path: [],
|
||||
value: replacement === NOTHING ? undefined : replacement
|
||||
})
|
||||
inversePatches_!.push({
|
||||
op: REPLACE,
|
||||
path: [],
|
||||
value: baseValue
|
||||
})
|
||||
}
|
||||
|
||||
function applyPatches_<T>(draft: T, patches: readonly Patch[]): T {
|
||||
patches.forEach(patch => {
|
||||
const {path, op} = patch
|
||||
|
||||
let base: any = draft
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
const parentType = getArchtype(base)
|
||||
let p = path[i]
|
||||
if (typeof p !== "string" && typeof p !== "number") {
|
||||
p = "" + p
|
||||
}
|
||||
|
||||
// See #738, avoid prototype pollution
|
||||
if (
|
||||
(parentType === ArchType.Object || parentType === ArchType.Array) &&
|
||||
(p === "__proto__" || p === CONSTRUCTOR)
|
||||
)
|
||||
die(errorOffset + 3)
|
||||
if (isFunction(base) && p === PROTOTYPE) die(errorOffset + 3)
|
||||
base = get(base, p)
|
||||
if (!isObjectish(base)) die(errorOffset + 2, path.join("/"))
|
||||
}
|
||||
|
||||
const type = getArchtype(base)
|
||||
const value = deepClonePatchValue(patch.value) // used to clone patch to ensure original patch is not modified, see #411
|
||||
const key = path[path.length - 1]
|
||||
switch (op) {
|
||||
case REPLACE:
|
||||
switch (type) {
|
||||
case ArchType.Map:
|
||||
return base.set(key, value)
|
||||
/* istanbul ignore next */
|
||||
case ArchType.Set:
|
||||
die(errorOffset)
|
||||
default:
|
||||
// if value is an object, then it's assigned by reference
|
||||
// in the following add or remove ops, the value field inside the patch will also be modifyed
|
||||
// so we use value from the cloned patch
|
||||
// @ts-ignore
|
||||
return (base[key] = value)
|
||||
}
|
||||
case ADD:
|
||||
switch (type) {
|
||||
case ArchType.Array:
|
||||
return key === "-"
|
||||
? base.push(value)
|
||||
: base.splice(key as any, 0, value)
|
||||
case ArchType.Map:
|
||||
return base.set(key, value)
|
||||
case ArchType.Set:
|
||||
return base.add(value)
|
||||
default:
|
||||
return (base[key] = value)
|
||||
}
|
||||
case REMOVE:
|
||||
switch (type) {
|
||||
case ArchType.Array:
|
||||
return base.splice(key as any, 1)
|
||||
case ArchType.Map:
|
||||
return base.delete(key)
|
||||
case ArchType.Set:
|
||||
return base.delete(patch.value)
|
||||
default:
|
||||
return delete base[key]
|
||||
}
|
||||
default:
|
||||
die(errorOffset + 1, op)
|
||||
}
|
||||
})
|
||||
|
||||
return draft
|
||||
}
|
||||
|
||||
// optimize: this is quite a performance hit, can we detect intelligently when it is needed?
|
||||
// E.g. auto-draft when new objects from outside are assigned and modified?
|
||||
// (See failing test when deepClone just returns obj)
|
||||
function deepClonePatchValue<T>(obj: T): T
|
||||
function deepClonePatchValue(obj: any) {
|
||||
if (!isDraftable(obj)) return obj
|
||||
if (isArray(obj)) return obj.map(deepClonePatchValue)
|
||||
if (isMap(obj))
|
||||
return new Map(
|
||||
Array.from(obj.entries()).map(([k, v]) => [k, deepClonePatchValue(v)])
|
||||
)
|
||||
if (isSet(obj)) return new Set(Array.from(obj).map(deepClonePatchValue))
|
||||
const cloned = Object.create(getPrototypeOf(obj))
|
||||
for (const key in obj) cloned[key] = deepClonePatchValue(obj[key])
|
||||
if (has(obj, immerable)) cloned[immerable] = obj[immerable]
|
||||
return cloned
|
||||
}
|
||||
|
||||
function clonePatchValueIfNeeded<T>(obj: T): T {
|
||||
if (isDraft(obj)) {
|
||||
return deepClonePatchValue(obj)
|
||||
} else return obj
|
||||
}
|
||||
|
||||
loadPlugin(PluginPatches, {
|
||||
applyPatches_,
|
||||
generatePatches_,
|
||||
generateReplacementPatches_,
|
||||
getPath
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user