Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d042639
perf(array): improve array remove method perf
unadlib Jan 3, 2025
a315808
refactor(array): refactor array remove
unadlib Jan 4, 2025
928eba8
test(benchmark): update
unadlib Jan 4, 2025
2ac6f36
chore: update
unadlib Jan 4, 2025
0119b61
refactor(array): update
unadlib Jan 4, 2025
1a497ca
fix(array): fix issue
unadlib Jan 4, 2025
8c51ceb
refactor(array): update
unadlib Jan 4, 2025
d761f0a
fix(array): update
unadlib Jan 4, 2025
b55b0b5
test(test): fix testing
unadlib Jan 4, 2025
a1832ad
fix(array): fix array handler
unadlib Jan 12, 2025
82de4bf
test(array): add array testing
unadlib Jan 13, 2025
aea600b
test(array): update
unadlib Jan 13, 2025
72c40a6
fix(snapshots): update
unadlib Jan 13, 2025
ad21888
refactor(array): update
unadlib Jan 13, 2025
f335be2
refactor(array): refactor array method proxy
unadlib Mar 9, 2025
5e6af9c
Revert "refactor(array): refactor array method proxy"
unadlib Mar 9, 2025
8592e66
test(array): add array testing
unadlib Mar 9, 2025
da67ae6
test: remove testing
unadlib Mar 16, 2025
e32acc1
test: update testing
unadlib Mar 16, 2025
a841fe8
refactor(array): imporve copyWithin
unadlib Mar 16, 2025
e516c43
fix(array): update
unadlib Mar 16, 2025
48240cf
feat(option): add enableOptimizedArray option
unadlib Mar 16, 2025
516a94b
test(array): add testing
unadlib Mar 16, 2025
b383abb
test(benchamark): update
unadlib Mar 16, 2025
ec45d87
chore(type): fix import type
unadlib Mar 16, 2025
fdd8cd6
docs(jsdoc): update
unadlib Mar 16, 2025
0bdf693
fix(array): update
unadlib Mar 18, 2025
f235b94
docs(array): update
unadlib Mar 23, 2025
9b9abf8
fix(patches): fix array patches
unadlib Mar 23, 2025
e6d8eac
test(test): update
unadlib Jul 21, 2025
8eb9719
refactor(enableoptimizedarray): remove enableOptimizedArray option
unadlib Sep 14, 2025
177f003
test(ut): update
unadlib Sep 14, 2025
abfbdf6
test(ut): update
unadlib Sep 14, 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
2 changes: 2 additions & 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 Expand Up @@ -117,6 +118,7 @@
"json2csv": "^5.0.7",
"lodash": "^4.17.21",
"lodash.clonedeep": "^4.5.0",
"mitata": "^1.0.34",
"prettier": "^3.6.2",
"quickchart-js": "^3.1.3",
"redux": "^5.0.1",
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
83 changes: 70 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,14 @@ const proxyHandler: ProxyHandler<ProxyDraft> = {
}
}
const source = latest(target);
const skipFinalization = target.options.skipFinalization;

if (
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 +97,34 @@ const proxyHandler: ProxyHandler<ProxyDraft> = {

if (!has(source, key)) {
const desc = getDescriptor(source, key);
const value = desc?.value;
if (
target.type === DraftType.Array &&
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 +137,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 +163,14 @@ const proxyHandler: ProxyHandler<ProxyDraft> = {
}
return target.copy![key];
}
if (
target.finalities.arrayHandling &&
!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 +276,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 +305,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 +323,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 +372,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
59 changes: 36 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 Down Expand Up @@ -161,8 +174,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 +184,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