Move isFluidHandle to shared-object-base#17328
Move isFluidHandle to shared-object-base#17328CraigMacomber wants to merge 21 commits intomicrosoft:mainfrom
Conversation
⯅ @fluid-example/bundle-size-tests: +3.49 KB
Baseline commit: 09c8d09 |
experimental/dds/tree2/src/feature-libraries/contextuallyTyped.ts
Outdated
Show resolved
Hide resolved
Co-authored-by: Mark Fields <markfields@users.noreply.github.com>
| // To help detect if the limitation of this old handle detection was impacting behavior, keep it around for now: | ||
| const isHandleLegacy = (value as Partial<IFluidHandle>)?.IFluidHandle !== undefined; | ||
| const isHandle = isFluidHandle(value); | ||
| assert(isHandleLegacy === isHandle, "new isFluidHandle should not change existing policy."); |
There was a problem hiding this comment.
this check is breaking some "ci:tst:realsvc:local" tests related to GC.
I don't know if thats finding a real issue (we have code which uses handles which do not have cyclic refs), or test issue (the test is doing something test specific that breaks this)
There was a problem hiding this comment.
I talked to @DLehenbauer About this and got some of the history for this pattern. Turns out that I think the assumption that these refs are cyclic might not be required to be true (but we could possibly add this requirement).
Alternatively we could make handle use symbols to identify themselves.
There was a problem hiding this comment.
I have updated this PR to permit noncyclic handles, changing how the detection works and rewriting most of the documentation.
There was a problem hiding this comment.
This still fails tests. All the docs in this code claim its trying to detect IFluidHandle, but the actual implementation seems to detect IProvideFluidHandle. My changes make it actually detect IFluidHandle, and this breaks a 14 of GC related end-to-end tests.
It is unclear to me if the tests are wrong, and happened to work due to incorrect detecting of IFluidHandles in this code, or if this code is documented incorrectly and should be made to detect and special case IProvideFluidHandle instances instead.
There was a problem hiding this comment.
From the CI run, its the same test failing 14 times because it runs in different compat mode. I believe the test is incorrect. Let me take a stab at fixing it.
There was a problem hiding this comment.
Since its possible others might have made the same or similar mistakes in their code, I have added a changeset to this PR detailing that this could be a problem and how to fix it.
markfields
left a comment
There was a problem hiding this comment.
Left some small suggestions, but looks good
| return extractContentMap(content); | ||
| } else if (content !== null && typeof content === "object") { | ||
| const flexNode = tryGetEditNode(content); | ||
| // Only run isFluidHandle if content can't be an unhydrated node, |
There was a problem hiding this comment.
I read the function description and code and can't quite tie this comment to what's happening here. Must be some implicit connection between if it's a Map and being "unhydrated"? Is "flex" a well-defined term? How does it relate to hydration etc?
| attachGraph: () => assert.fail(), | ||
| bind: () => assert.fail(), | ||
| absolutePath: "", | ||
| } satisfies IFluidHandle), |
There was a problem hiding this comment.
Wasn't familiar with satisfies, looked it up, very cool! Always glad to learn to TS features from you!
| * Data Store serializer implementation. | ||
| * | ||
| * @privateRemarks | ||
| * Since this type is package exported (not just the Interface above), |
There was a problem hiding this comment.
It's @internal now! I had wanted to stop it from being public anyway, and it just happened in bulk. Maybe it's kind to wait until after internal.8.0 "just in case", but we should be good to change stuff like this with more liberty now AFAIK.
| // Detect if 'value' is an IFluidHandle. | ||
| const handle = value?.IFluidHandle; | ||
| // To help detect if the limitation of this old handle detection was impacting behavior, keep it around for now: | ||
| const isHandleLegacy = (value as Partial<IFluidHandle>)?.IFluidHandle !== undefined; |
There was a problem hiding this comment.
I'm always torn on asserts like the one below. If there's a case out there that violates it (like you found), this code will obviously throw. But is the calling code prepared to properly handle that throw? It's hard to anticipate exactly how that error will manifest. But I guess what else can you do. It's a great validation that our test (now) don't hit it.
No suggestion here, it's fine as-is, just thinking out loud.
| * (due to decodeValue's use of isSerializedHandle causing them to be wrongly parsed as handles) and should be avoided. | ||
| * @public | ||
| */ | ||
| export type FluidSerializableReadOnly = |
There was a problem hiding this comment.
@jason-ha has a PR out about some similar recursive types, is this new type harmonious with whatever he's doing? I haven't looked as his PR yet.
There was a problem hiding this comment.
This will not work with the current type test support.
TypeOnly utility will not be able to handle anything this such a type in it. (Although currently if this only appears two levels of properties down the error may not be observed.)
See #18523 that injects an interface where there is recursion to workaround the TypeOnly issue.
| | null | ||
| | readonly FluidSerializableReadOnly[] | ||
| | { readonly [P in string]?: FluidSerializableReadOnly }; | ||
|
|
There was a problem hiding this comment.
How would you contrast [P in string] v. something like [P in keyof object]? I guess Symbol keys would be excluded, not sure how it would play out. How did you land on P in string?
| /** | ||
| * Check if a value in {@link FluidSerializableReadOnly} data is an {@link @fluidframework/core-interfaces#IFluidHandle}. | ||
| * | ||
| * Warning: Non-IFluidHandle objects with a key "IFluidHandle" will cause an assert. |
There was a problem hiding this comment.
Why assert instead of merely returning false? Seems unnecessarily disruptive if a violation were to happen.
There was a problem hiding this comment.
Ok I read the comment in the implementation. Similar to the assert in encodeValue - it's about failing fast during this transition to stricter runtime checks.
Do you expect to come back later and remove these asserts?
| return false; | ||
| } | ||
| const partialHandle = value as Partial<IFluidHandle>; | ||
| const innerHandle = partialHandle.IFluidHandle; |
There was a problem hiding this comment.
Given the pattern where innerHandle === value, calling it innerHandle could be misleading to an outside. Maybe add a comment saying something like nnerHandle is expected to be a cyclical reference to value itself, not a secondary "inner" handle.
| ); | ||
|
|
||
| assert( | ||
| innerHandle.IFluidHandle?.IFluidHandle?.IFluidHandle?.IFluidHandle !== undefined, |
| * @privateRemarks | ||
| * Any of the following changes to IFluidHandle would solve the ambiguity problem: | ||
| * | ||
| * - The "IFluidHandle" property could be required to be a cyclic reference back to the parent. |
There was a problem hiding this comment.
It's pretty tempting to just enforce this. Maybe after the next release we enforce that further restriction.
| // If the object fails this check, it will assert to detect problematic data that would have incorrectly been treated as a handle in previous versions of the serializer. | ||
|
|
||
| // Since json compatible data shouldn't have methods, and IFluidHandle requires one, use that as a redundant check: | ||
| const getMember = (value as Partial<IFluidHandle>).get; |
There was a problem hiding this comment.
Since isFluidHandle is called repeatedly/recursively on a critical path (op processing), I'm wondering if we need to deliberately optimize it. Putting this comment here because maybe we put this under the if (value !== innerHandle) check so it (likely) never actually has to run.
But the comment appllies in general, but the happy path of the function looks pretty lean. Do we have any perf benchmarks that cover this code that we could observe before/after? I think we do but not sure where or how to intepret them.
markfields
left a comment
There was a problem hiding this comment.
Left plenty of comments to consider, but nothing blocking. Nice incremental change to make!
jason-ha
left a comment
There was a problem hiding this comment.
FluidSerializableReadOnly needs altered to allow TypeOnly to wrap it.
Currently:
declare function use_current_TypeAlias_FluidSerializableReadOnly(
use: TypeOnly<current.FluidSerializableReadOnly>): void;
will result in
@fluidframework/shared-object-base: [cjs]: src/test/types/validateSharedObjectBasePrevious.generated.ts:25:10 - error TS2589: Type instantiation is excessively deep and possibly infinite.
@fluidframework/shared-object-base:
@fluidframework/shared-object-base: 25 use: TypeOnly<current.FluidSerializableReadOnly>): void;
(The type test generator should really create a declaration for all types in current, even if not present in the old to expose such an issue.)
…to isFluidHandle
| const getMember = (value as Partial<IFluidHandle>).get; | ||
| assert( | ||
| typeof getMember === "function", | ||
| "Fluid handle detection found IFluidHandle field, but not have a get method", |
There was a problem hiding this comment.
...but +does+ not have or similar wording fix needed
|
I think the best thing to do here is fix the fact that its impossible to robustly detect handles, which can be done by using a class or symbol for the handles. I believe https://dev.azure.com/fluidframework/internal/_workitems/edit/1507 is tracking this internally. If we are unable to make handles more robust, then maybe landing something like this PR makes sense. |
|
This PR has been automatically marked as stale because it has had no activity for 60 days. It will be closed if no further activity occurs within 8 days of this comment. Thank you for your contributions to Fluid Framework! |
Description
FluidSeralizer now uses a more robust check and more documented types for its IFluidHandle detection.
Once an assert (to detect data where the behavior changed, which we expect currently not to be present) is removed, this will allow FluidSeralizer to safely handle arbitrary object keys (for example user provided strings). Before this change, if such data happend to be an object key "IFluidHandle", it would treat it as an
IFluidHandle.Further adopting the new FluidSerializableReadOnly in the API will be a breaking change, and thus is not part of this PR.
Breaking Changes
FluidSeralizer will now treat objects with a "IFluidHandle" field that is not a IFluidHandle (or is one but doesn't refer to itself in its .IFluidHandle field) as not a fluid handle.
This is not expected to break any usages, but any data impacted by this change will now assert.
Reviewer Guidance
The review process is outlined on this wiki page.