Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f8dce6c
perf(array): improve array remove method perf
unadlib Jan 3, 2025
f5cac1d
refactor(array): refactor array remove
unadlib Jan 4, 2025
3d51456
test(benchmark): update
unadlib Jan 4, 2025
27c3eb5
chore: update
unadlib Jan 4, 2025
f028c38
refactor(array): update
unadlib Jan 4, 2025
dfc7bf2
fix(array): fix issue
unadlib Jan 4, 2025
688c3a1
refactor(array): update
unadlib Jan 4, 2025
bf62379
fix(array): update
unadlib Jan 4, 2025
f3b1d9a
test(test): fix testing
unadlib Jan 4, 2025
8832a3b
fix(array): fix array handler
unadlib Jan 12, 2025
2e55a8b
test(array): add array testing
unadlib Jan 13, 2025
b623ce3
test(array): update
unadlib Jan 13, 2025
f9bbbcb
fix(snapshots): update
unadlib Jan 13, 2025
891aa42
refactor(array): update
unadlib Jan 13, 2025
c880828
refactor(array): refactor array method proxy
unadlib Mar 9, 2025
62273fa
Revert "refactor(array): refactor array method proxy"
unadlib Mar 9, 2025
00f0d97
test(array): add array testing
unadlib Mar 9, 2025
00b2d98
test: remove testing
unadlib Mar 16, 2025
4982bff
test: update testing
unadlib Mar 16, 2025
acff875
refactor(array): imporve copyWithin
unadlib Mar 16, 2025
81105c8
fix(array): update
unadlib Mar 16, 2025
cd52eb7
feat(option): add enableOptimizedArray option
unadlib Mar 16, 2025
a31db4f
test(array): add testing
unadlib Mar 16, 2025
059b960
test(benchamark): update
unadlib Mar 16, 2025
7a33292
chore(type): fix import type
unadlib Mar 16, 2025
4c651b9
docs(jsdoc): update
unadlib Mar 16, 2025
beda104
fix(array): update
unadlib Mar 18, 2025
5345888
docs(array): update
unadlib Mar 23, 2025
74ee865
fix(patches): fix array patches
unadlib Mar 23, 2025
9fe20cb
test(test): update
unadlib Jul 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"benchmark": "yarn build && yarn benchmark:base && yarn benchmark:object && yarn benchmark:array && yarn benchmark:class",
"all-benchmark": "yarn build && NODE_ENV='production' ts-node test/benchmark/index.ts",
"benchmark:reducer": "NODE_ENV='production' ts-node test/performance/benchmark-reducer.ts",
"benchmark:reducer1": "NODE_ENV='production' node test/performance/benchmark-reducer1.mjs",
"benchmark:base": "NODE_ENV='production' ts-node test/performance/benchmark.ts",
"benchmark:object": "NODE_ENV='production' ts-node test/performance/benchmark-object.ts",
"benchmark:array": "NODE_ENV='production' ts-node test/performance/benchmark-array.ts",
Expand Down
2 changes: 1 addition & 1 deletion src/current.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type Draft, DraftType, type ProxyDraft } from './interface';
import { DraftType, type Draft, type ProxyDraft } from './interface';
import {
forEach,
get,
Expand Down
86 changes: 73 additions & 13 deletions src/draft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import {
Finalities,
Patches,
ProxyDraft,
Options,
Operation,
DraftOptions,
} from './interface';
import { dataTypes, PROXY_DRAFT } from './constant';
import { mapHandler, mapHandlerKeys } from './map';
Expand All @@ -29,17 +29,22 @@ import {
finalizeSetValue,
markFinalization,
finalizePatches,
isDraft,
} from './utils';
import { checkReadable } from './unsafe';
import { generatePatches } from './patch';

const draftsCache = new WeakSet<object>();
// The array methods that need to be handled by the draft.
// `sort` is not included, because array items may be modified by mutations in the sort function, it has to be drafted.
// `copyWithin` is not included, it would require implementing a complete check of array copy reference relationships,
// which might result in limited performance gains and increased maintenance complexity.
const proxyArrayMethods = ['splice', 'shift', 'unshift', 'reverse'];

const proxyHandler: ProxyHandler<ProxyDraft> = {
get(target: ProxyDraft, key: string | number | symbol, receiver: any) {
const copy = target.copy?.[key];
// Improve draft reading performance by caching the draft copy.
if (copy && draftsCache.has(copy)) {
if (copy && target.finalities.draftsCache.has(copy)) {
return copy;
}
if (key === PROXY_DRAFT) return target;
Expand All @@ -61,6 +66,15 @@ const proxyHandler: ProxyHandler<ProxyDraft> = {
}
}
const source = latest(target);
const skipFinalization = target.options.skipFinalization;

if (
skipFinalization &&
source[key] &&
target.finalities.draftsCache.has(source[key])
) {
return source[key];
}

if (source instanceof Map && mapHandlerKeys.includes(key as any)) {
if (key === 'size') {
Expand All @@ -84,9 +98,35 @@ const proxyHandler: ProxyHandler<ProxyDraft> = {

if (!has(source, key)) {
const desc = getDescriptor(source, key);
const value = desc?.value;
if (
target.type === DraftType.Array &&
skipFinalization &&
proxyArrayMethods.includes(key as string)
) {
return function (this: any, ...args: any[]) {
let returnValue: any;
target.finalities.arrayHandling = true;
try {
returnValue = value.apply(this, args);
if (isDraftable(returnValue, target.options)) {
returnValue = createDraft({
original: returnValue,
parentDraft: undefined,
key: undefined,
finalities: target.finalities,
options: target.options,
});
}
return returnValue;
} finally {
target.finalities.arrayHandling = false;
}
};
}
return desc
? `value` in desc
? desc.value
? value
: // !case: support for getter
desc.get?.call(target.proxy)
: undefined;
Expand All @@ -99,10 +139,17 @@ const proxyHandler: ProxyHandler<ProxyDraft> = {
return value;
}
// Ensure that the assigned values are not drafted
if (value === peek(target.original, key)) {
if (
!target.finalities.arrayHandling &&
(value === peek(target.original, key) || skipFinalization?.has(value))
) {
const shouldSkip = skipFinalization?.has(value);
if (shouldSkip) {
skipFinalization!.delete(value);
}
ensureShallowCopy(target);
target.copy![key] = createDraft({
original: target.original[key],
original: shouldSkip ? target.copy![key] : target.original[key],
parentDraft: target,
key: target.type === DraftType.Array ? Number(key) : key,
finalities: target.finalities,
Expand All @@ -118,6 +165,15 @@ const proxyHandler: ProxyHandler<ProxyDraft> = {
}
return target.copy![key];
}
if (
target.finalities.arrayHandling &&
skipFinalization &&
!isDraft(value) &&
isDraftable(value)
) {
// !case: handle the case of assigning the original array item via array methods(`splice`, `shift``, `unshift`, `reverse`)
skipFinalization.add(value);
}
return value;
},
set(target: ProxyDraft, key: string | number | symbol, value: any) {
Expand Down Expand Up @@ -223,7 +279,7 @@ export function createDraft<T extends object>(createDraftOptions: {
parentDraft?: ProxyDraft | null;
key?: string | number | symbol;
finalities: Finalities;
options: Options<any, any>;
options: DraftOptions;
}): T {
const { original, parentDraft, key, finalities, options } =
createDraftOptions;
Expand Down Expand Up @@ -252,7 +308,7 @@ export function createDraft<T extends object>(createDraftOptions: {
proxyHandler
);
finalities.revoke.push(revoke);
draftsCache.add(proxy);
finalities.draftsCache.add(proxy);
proxyDraft.proxy = proxy;
if (parentDraft) {
const target = parentDraft;
Expand All @@ -270,7 +326,11 @@ export function createDraft<T extends object>(createDraftOptions: {
}
finalizeSetValue(proxyDraft);
finalizePatches(proxyDraft, generatePatches, patches, inversePatches);
if (__DEV__ && target.options.enableAutoFreeze) {
if (
__DEV__ &&
target.options.enableAutoFreeze &&
typeof updatedValue === 'object'
) {
target.options.updatedValues =
target.options.updatedValues ?? new WeakMap();
target.options.updatedValues.set(updatedValue, proxyDraft.original);
Expand Down Expand Up @@ -315,10 +375,10 @@ export function finalizeDraft<T>(
const state = hasReturnedValue
? returnedValue[0]
: proxyDraft
? proxyDraft.operated
? proxyDraft.copy
: proxyDraft.original
: result;
? proxyDraft.operated
? proxyDraft.copy
: proxyDraft.original
: result;
if (proxyDraft) revokeProxy(proxyDraft);
if (enableAutoFreeze) {
deepFreeze(state, state, proxyDraft?.options.updatedValues);
Expand Down
10 changes: 6 additions & 4 deletions src/draftify.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
import type {
DraftOptions,
Finalities,
Options,
Patches,
PatchesOptions,
Result,
Expand All @@ -12,15 +12,17 @@ import { dataTypes } from './constant';
export function draftify<
T extends object,
O extends PatchesOptions = false,
F extends boolean = false
F extends boolean = false,
>(
baseState: T,
options: Options<O, F>
options: DraftOptions
): [T, (returnedValue: [T] | []) => Result<T, O, F>] {
const finalities: Finalities = {
draft: [],
revoke: [],
handledSet: new WeakSet<any>(),
draftsCache: new WeakSet<object>(),
arrayHandling: false,
};
let patches: Patches | undefined;
let inversePatches: Patches | undefined;
Expand Down
63 changes: 40 additions & 23 deletions src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export interface Finalities {
draft: ((patches?: Patches, inversePatches?: Patches) => void)[];
revoke: (() => void)[];
handledSet: WeakSet<any>;
draftsCache: WeakSet<object>;
arrayHandling: boolean;
}

export interface ProxyDraft<T = any> {
Expand All @@ -42,7 +44,7 @@ export interface ProxyDraft<T = any> {
copy: T | null;
proxy: T | null;
finalities: Finalities;
options: Options<any, any> & { updatedValues?: WeakMap<any, any> };
options: DraftOptions;
parent?: ProxyDraft | null;
key?: string | number | symbol;
setMap?: Map<any, ProxyDraft>;
Expand All @@ -62,30 +64,30 @@ export type Patch<P extends PatchesOptions = any> = P extends {
path: string;
}
: P extends true | object
? IPatch & {
path: (string | number)[];
}
: IPatch & {
path: string | (string | number)[];
};
? IPatch & {
path: (string | number)[];
}
: IPatch & {
path: string | (string | number)[];
};

export type Patches<P extends PatchesOptions = any> = Patch<P>[];

export type Result<
T extends any,
O extends PatchesOptions,
F extends boolean
F extends boolean,
> = O extends true | object
? [F extends true ? Immutable<T> : T, Patches<O>, Patches<O>]
: F extends true
? Immutable<T>
: T;
? Immutable<T>
: T;

export type CreateResult<
T extends any,
O extends PatchesOptions,
F extends boolean,
R extends void | Promise<void> | T | Promise<T>
R extends void | Promise<void> | T | Promise<T>,
> = R extends Promise<void> | Promise<T>
? Promise<Result<T, O, F>>
: Result<T, O, F>;
Expand All @@ -99,8 +101,8 @@ export type Mark<O extends PatchesOptions, F extends boolean> = (
) => O extends true | object
? BaseMark
: F extends true
? BaseMark
: MarkWithCopy;
? BaseMark
: MarkWithCopy;

export interface ApplyMutableOptions {
/**
Expand Down Expand Up @@ -129,6 +131,17 @@ export interface Options<O extends PatchesOptions, F extends boolean> {
mark?: Mark<O, F>;
}

export type DraftOptions = Options<any, any> & {
/**
* a collection for circular reference check
*/
updatedValues?: WeakMap<any, any>;
/**
* a collection for array item skip deep check
*/
skipFinalization?: WeakSet<any>;
};

export interface ExternalOptions<O extends PatchesOptions, F extends boolean> {
/**
* In strict mode, Forbid accessing non-draftable values and forbid returning a non-draft value.
Expand All @@ -147,6 +160,10 @@ export interface ExternalOptions<O extends PatchesOptions, F extends boolean> {
* And it can also return a shallow copy function(AutoFreeze and Patches should both be disabled).
*/
mark?: Mark<O, F>[] | Mark<O, F>;
/**
* Enable optimized array for improving performance.
*/
enableOptimizedArray?: boolean;
}

// Exclude `symbol`
Expand All @@ -161,8 +178,8 @@ export type IfAvailable<T, Fallback = void> = true | false extends (
)
? Fallback
: keyof T extends never
? Fallback
: T;
? Fallback
: T;
type WeakReferences =
| IfAvailable<WeakMap<any, any>>
| IfAvailable<WeakSet<any>>;
Expand All @@ -171,14 +188,14 @@ type AtomicObject = Function | Promise<any> | Date | RegExp;
export type Immutable<T> = T extends Primitive | AtomicObject
? T
: T extends IfAvailable<ReadonlyMap<infer K, infer V>>
? ImmutableMap<K, V>
: T extends IfAvailable<ReadonlySet<infer V>>
? ImmutableSet<V>
: T extends WeakReferences
? T
: T extends object
? ImmutableObject<T>
: T;
? ImmutableMap<K, V>
: T extends IfAvailable<ReadonlySet<infer V>>
? ImmutableSet<V>
: T extends WeakReferences
? T
: T extends object
? ImmutableObject<T>
: T;

type DraftedMap<K, V> = Map<K, Draft<V>>;
type DraftedSet<T> = Set<Draft<T>>;
Expand Down
Loading