Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
65 changes: 47 additions & 18 deletions packages/table-core/src/core/table/coreTablesFeature.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ export function table_reset<
* Merges new table options with the current resolved options.
*
* If `options.mergeOptions` is provided, it owns the merge behavior; otherwise
* options are shallow-merged.
* options are shallow-merged. Static options that should never change after
* initialization are restored on a fresh object so framework merge helpers may
* return readonly getter/proxy objects.
*
* @example
* ```ts
Expand All @@ -82,17 +84,52 @@ export function table_mergeOptions<
table: Table_Internal<TFeatures, TData>,
newOptions: TableOptions<TFeatures, TData>,
) {
if (table.options.mergeOptions) {
return table.options.mergeOptions(
table.options as TableOptions<TFeatures, TData>,
newOptions,
)
const { features, atoms, initialState } = table.options

// simple merge if no mergeOptions is provided - performant
if (!table.options.mergeOptions) {
return {
...table.options,
...newOptions,
features,
atoms,
initialState,
}
}

return {
...table.options,
...newOptions,
// else use the mergeOptions function and preserve getters/setters
const mergedOptions = table.options.mergeOptions(
table.options as TableOptions<TFeatures, TData>,
newOptions,
)
const descriptors: PropertyDescriptorMap = {
...Object.getOwnPropertyDescriptors(mergedOptions),
}

return Object.defineProperties(
Object.create(Object.getPrototypeOf(mergedOptions)),
{
...descriptors,
features: {
value: features,
enumerable: true,
configurable: true,
writable: true,
},
atoms: {
value: atoms,
enumerable: true,
configurable: true,
writable: true,
},
initialState: {
value: initialState,
enumerable: true,
configurable: true,
writable: true,
},
},
) as TableOptions<TFeatures, TData>
}

/**
Expand All @@ -117,15 +154,7 @@ export function table_setOptions<
updater,
table.options as TableOptions<TFeatures, TData>,
)
// table static options that should never change after initialization
const { features, atoms, initialState } = table.options
const mergedOptions = Object.assign(table_mergeOptions(table, newOptions), {
// Once the table instance is created those properties should never change after initialization,
// so we assign them back preserving the `table_mergeOptions` object reference
features,
atoms,
initialState,
})
const mergedOptions = table_mergeOptions(table, newOptions)

if (table.optionsStore) {
table.optionsStore.set(() => mergedOptions)
Expand Down
72 changes: 72 additions & 0 deletions packages/table-core/tests/unit/core/table/constructTable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,36 @@ import { describe, expect, it } from 'vitest'
import { constructTable, coreFeatures } from '../../../../src'
import { storeReactivityBindings } from '../../../../src/store-reactivity-bindings'

function getterOnlyMerge(...sources: Array<any>) {
const target = {}

for (const source of sources) {
if (!source) {
continue
}

for (const key of Reflect.ownKeys(source)) {
if (key in target) {
continue
}

Object.defineProperty(target, key, {
enumerable: true,
get() {
for (let i = sources.length - 1; i >= 0; i--) {
const value = sources[i]?.[key]
if (value !== undefined) {
return value
}
}
},
})
}
}

return target
}

describe('constructTable', () => {
it('should create a table with all core table APIs and properties', () => {
const table = constructTable({
Expand Down Expand Up @@ -46,4 +76,46 @@ describe('constructTable', () => {
expect(table).toHaveProperty('setOptions')
expect(table).toHaveProperty('store') // state is managed via store in v9
})

it('preserves static options without mutating mergeOptions results', () => {
const features = {
...coreFeatures,
coreReactivityFeature: storeReactivityBindings(),
}
const atoms = {}
const initialState = {}
const data: Array<{ id: number }> = []
const nextData = [{ id: 1 }]
const nextFeatures = {
...coreFeatures,
coreReactivityFeature: storeReactivityBindings(),
}
const nextAtoms = {}
const nextInitialState = {}

const table = constructTable<typeof features, { id: number }>({
features,
atoms,
initialState,
columns: [],
data,
mergeOptions: (defaultOptions, options) =>
getterOnlyMerge(defaultOptions, options) as any,
})

expect(() => {
table.setOptions((prev) => ({
...prev,
data: nextData,
features: nextFeatures,
atoms: nextAtoms,
initialState: nextInitialState,
}))
}).not.toThrow()

expect(table.options.data).toBe(nextData)
expect(table.options.features).toBe(features)
expect(table.options.atoms).toBe(atoms)
expect(table.options.initialState).toBe(initialState)
})
})
Loading