-
Notifications
You must be signed in to change notification settings - Fork 96
refactor(zero-solid): reconcile changes to data due to query changes #4559
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
|
Branch | grgbkr/solid-reconcile |
Testbed | Linux |
Click to view all benchmark results
Benchmark | Throughput | Benchmark Result operations / second (ops/s) x 1e3 (Result Δ%) | Lower Boundary operations / second (ops/s) x 1e3 (Limit %) |
---|---|---|---|
src/client/custom.bench.ts > big schema | 📈 view plot 🚷 view threshold | 338.03 ops/s x 1e3(-4.78%)Baseline: 355.02 ops/s x 1e3 | 327.67 ops/s x 1e3 (96.93%) |
src/client/zero.bench.ts > basics > All 1000 rows x 10 columns (numbers) | 📈 view plot 🚷 view threshold | 1.62 ops/s x 1e3(+1.94%)Baseline: 1.59 ops/s x 1e3 | 1.55 ops/s x 1e3 (95.53%) |
src/client/zero.bench.ts > pk compare > pk = N | 📈 view plot 🚷 view threshold | 29.92 ops/s x 1e3(-1.38%)Baseline: 30.34 ops/s x 1e3 | 29.54 ops/s x 1e3 (98.70%) |
src/client/zero.bench.ts > with filter > Lower rows 500 x 10 columns (numbers) | 📈 view plot 🚷 view threshold | 2.53 ops/s x 1e3(+0.95%)Baseline: 2.51 ops/s x 1e3 | 2.44 ops/s x 1e3 (96.39%) |
FYI @devagrawal09 |
Thanks for creating this PR. What's the plan for adding ids? Also is it possible that when a new query is constructed, the data returned from zero has the same references? We might not need ids if the data is always kept in memory by zero and reused across queries, because |
We can definitely do same references. That might be simpler and more performant |
|
Branch | grgbkr/solid-reconcile |
Testbed | Linux |
Click to view all benchmark results
Benchmark | File Size | Benchmark Result kilobytes (KB) (Result Δ%) | Upper Boundary kilobytes (KB) (Limit %) |
---|---|---|---|
zero-package.tgz | 📈 view plot 🚷 view threshold | 1,209.32 KB(+0.01%)Baseline: 1,209.18 KB | 1,233.36 KB (98.05%) |
zero.js | 📈 view plot 🚷 view threshold | 199.60 KB(0.00%)Baseline: 199.60 KB | 203.59 KB (98.04%) |
zero.js.br | 📈 view plot 🚷 view threshold | 55.91 KB(0.00%)Baseline: 55.91 KB | 57.03 KB (98.04%) |
packages/zero-solid/package.json
Outdated
"@rocicorp/eslint-config": "^0.7.0", | ||
"@rocicorp/prettier-config": "^0.3.0", | ||
"@solidjs/testing-library": "^0.8.10", | ||
"jsdom": "^26.1.0", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why? We are running the tests in a real browser, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed
|
||
const complete = {type: 'complete'} as const; | ||
const unknown = {type: 'unknown'} as const; | ||
export const COMPLETE: QueryResultDetails = {type: 'complete'} as const; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should freeze these to prevent more surprises
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done. this actually modifies solid's behavior as it checks for frozen objects and copies them, but i think its best not to rely on it.
[this.#state, this.#setState] = createStore<State>([ | ||
|
||
this.#setState = setState; | ||
const s = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
inline?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove commented out logs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
const [state, setState] = createStore<State>([ | ||
{ | ||
'': undefined, | ||
}, | ||
{type: 'unknown'}, | ||
]); | ||
const data = () => state[0]['']; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe const {data, state, setState} = createTestStore()
packages/zero-solid/src/use-zero.ts
Outdated
MD extends CustomMutatorDefs<S> | undefined = undefined, | ||
>(props: { | ||
children: JSX.Element; | ||
zeroSignal: () => ZeroOptions<S, MD> | {zero: Zero<S, MD>}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find this type a bit strange. I want the props to be called zero and options.
Is this how the React one is done?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I fixed this to more closely match the React API. So its either:
<ZeroProvider zero={zero}>...</ZeroProvider>
or
<ZeroProvider server="https://foo.com" userID="u1" schema={schema}>...</ZeroProvider>
}); | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
const MockZeroProvider = (props: {children: JSX.Element}) => ( | ||
<ZeroProvider zeroSignal={zeroOptions}>{props.children}</ZeroProvider> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<ZeroProvider zeroSignal={zeroOptions}>{props.children}</ZeroProvider> | |
<ZeroProvider options={zeroOptions}>{props.children}</ZeroProvider> |
I hope
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i made this match the React version, so it would be something like:
<ZeroProvider server="https://foo.com" userID="u1" schema={schema}>
}); | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
const MockZeroProvider = (props: {children: JSX.Element}) => ( | ||
<ZeroProvider zeroSignal={zeroSignal}>{props.children}</ZeroProvider> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<ZeroProvider zeroSignal={zeroSignal}>{props.children}</ZeroProvider> | |
<ZeroProvider zero={zeroSignal}>{props.children}</ZeroProvider> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
|
||
return ZeroContext.Provider({ | ||
value: zero, | ||
get children() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so it seems this getter for children was the secret sauce... but why?
Why does the following not work
return ZeroContext.Provider({
value: zero,
children: props.children
});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should value also be a getter? There is still something fundamental about solidjs that i dont grok.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah you can't use JSX here
In that case the getter is correct.
value would also need to be a getter.
In solid when you access something from the props like props.value
inside a reactive scope, it needs to behave similar to accessing a signal value()
so that it can be tracked as a dependency. So the JSX transform changes <A b={c()} />
to A({ get b() { return c{} } })
.
This makes sure that every property passed through the component tree can be tracked independently.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
children
needs to be a getter to get the state tracking.
In the case of value
, zero
is already a signal so there is no need to use a getter.
return ZeroContext.Provider({ | ||
value: zero, | ||
get children() { | ||
return props.children; | ||
}, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return ZeroContext.Provider({ | |
value: zero, | |
get children() { | |
return props.children; | |
}, | |
}); | |
return <ZeroContext.Provider value={zero()}> | |
{props.children} | |
</ZeroContext.Provider> |
You should be able to do just this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to avoid using JSX here as esbuild doesn't seem able to build a library that uses both React JSX and Solid JSX (at least we couldn't figure it out).
3c5e68f
to
3489205
Compare
We do return the same references for rows that haven't changed. Let me do some testing and see if i can reconcile based on references working. |
…ata on query change and transaction commit
…omputed and enhance reconcile options
4d27167
to
40fe936
Compare
We should also be able to "merge" the rows that changed if the primary keys didn't change. This can be dealt with in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
}); | ||
createEffect(() => { | ||
const details = resultDetails(); | ||
details.type; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no op?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it does set up the tracking?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah necessary to set up the tracking
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice and extensive tests!
return zero ?? (() => undefined); | ||
} | ||
|
||
export function createUseZero< |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could use some JSDoc.
@devagrawal09 @arv |
@devagrawal09 Should we file a bug/pr on Solid to allow the key to be a symbol? |
Starting from this PR contribution: #4474
Prior to this change the zero-solid integration created a new store whenever a query changed.
This caused everything depending on the store to be disposed and recreated, instead of receiving granular updates.
This PR ensures that every
createQuery
call only creates a store the first time and passes on the setter to the view, and the view is responsible for reconciling data when there's large changes.ZeroProvider
anduseZero
so that the zero instance can be accessed through context,ZeroProvider
can take a signal for aZeroOptions
or aZero
instance.useZero
return a signal ofZero
instance since the signal to ZeroProvider can change theZeroOptions
orZero
instance.useQuery
since it now requires context. DeprecatescreateQuery
instead.useX
is a good convention to communicate that this should be called inside a component and not outside.createUseZero
to prevent redundant annotations, and so the api mirror's React's apis much more closely.Next steps:
reconcile
utility to achieve granular updates for they initial hydration of a changed query. This will require adding synthetic ids to rows.