-
Notifications
You must be signed in to change notification settings - Fork 661
[WIP] ui: implement UI server extension #4192
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
base: main
Are you sure you want to change the base?
Conversation
This is an initial implementaton of RFC-0005 (#3227).
primiano
left a comment
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 left some comments. overall LG with few minor comments here and there.
to me the biggest blocker part that needs to be figured out is the interaction between core and this.
my theory is that this cannot be a pure extension (sorry for the pun). the core needs to know "something" about dynamically loadable extensions.
What I don't like is the fragile dance, where this extension loads, and then injects stuff into core, and then some other code needs to carefully await that. it's very very brittle.
I don't have a fully fledged proposal on top off my head, but let's discuss more tomorrow (book some time, steve should be also around).
In my mind the core needs to know, at least in part, of server extensions.
At least part of that types.json imho should be in core.
I don't think core needs to know about how to fetch stuff (Which can be delegated to a plugin like you did).
But in my mind core needs to know at least that "some plugins can provide server extensions, and when you need stuff like sql modules or descriptors, the core needs to query them"
This is the problem I see:
- Some of the stuff that a server offers, in theory is push-only, e.g. macros which could be injected at any time.
-I say in "theory" because in reality we are better off ensuring that they are registered very early on. because somebody might end up triggering a macro early on (can they do progammatically) and being confused about the fact it's not there (unless we say macros for now can only be invoked via user action) - some other stuff instead is "pull": for instance sql modules. there is a precise point in time, i believe, when core initializes TP, where all the modules MUST have been gathered. same for proto descriptors (maybe?)
this is what needs to be defined.
If there is something that is a "pull at a specific point in time (e.g. while initializing TP)" core must know about that concept, and the dependency needs to be handled with something like "your plugin when initializes sets something into core like registerSqlModuleProvider(...)"
the only question I have is whether over time we'll end up promoting 80% of the extensions bits to the core, at which point we are better off promoting the whole extension thing right now?
this is the biggest open question i have: some part of this MUST be promoted to core: how much ?
| extensionMacrosDeferred = defer<void>(); | ||
| extensionSqlModulesDeferred = defer<void>(); | ||
| extensionProtoDescriptorsDeferred = defer<void>(); | ||
|
|
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.
if you do it this way this will 100% lead to bugs, as this, as-is, is a pure gentlement agreement for "you are supposed to await the xxxDeferred before looking at the actual data thing"
Imho it's way more robust if the deferred thing carries the data you need, (so it's like extensionMacrosDeferred = defer()) so you can't possibly miss the await
| } | ||
|
|
||
| // Wait for extension proto descriptors to load before using them | ||
| await app.extensionProtoDescriptorsDeferred; |
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 have a bit of a question on whether we should do these fine-grained deferred things.
On one side, they avoid blocking for a big load before you can use the UI.
OTOH they create large opportunities for "the UI silently stops working in the middle of somewhere" if a network fetch fails.
If you want to break down and do fine-grained deferred, at very least they should be wrapper in something that has a "orTimeout" so we don't hang forever, and give a soft-error in the omnibox (and in future in analytics)
| // Await the promises: we've tried to be async as long as possible but | ||
| // now we need the extras to be loaded. | ||
| await app.extraLoadingPromise; | ||
| await app.extensionMacrosDeferred; |
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.
(reinforcing my previous comment) this programming model is unsustainable. there are magic awaits. Try to open this code in 3 months and go "why there is this await here? it seems to work if I remove it"
| @@ -0,0 +1,299 @@ | |||
| // Copyright (C) 2024 The Android Open Source Project | |||
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.
here and elsewhere: 2026
| // ============================================================================= | ||
|
|
||
| /** | ||
| * A single proto descriptor entry. |
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.
Can you either keep the schemas in order (from top-level to nested) or add a comment with some sort of tree that explains how everything fits with each other.
it's not obvious just by reading this file.
Also this one itself deserves a comment to epplain what this is, what this is for and how it fits in the overall tree.
it's not obvious to 90% of people what a proto descriptor does and is for.
| schema: z.ZodSchema, | ||
| ): Promise<T | undefined> { | ||
| try { | ||
| const response = await fetch(url); |
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 use fetchWithTimeout so we don't hang forever? I think chrome's default timeout is like 5 mins, a bit too long
also a bit rusty on this, but I think this should be
fetchJson<T>(url:string, schema: T): Promise<z.infer<typeof T>|undefined>
to link the type of the schema and the returned type
also why do you need the | undefined part? shouldn't you bubble up fetch errors so that if somebody writes the wrong manifest with invalid stuff (or the server returns some HTTP 500) it becomes obvious?
Either that (i.e. a crash, which is better imho), or wrap in to some exception that we handled in a managed way so we don't propmpt users to file a bug.
but swallowing silently is going to hide server-side bugs.
| // Runtime State Management | ||
| // ============================================================================= | ||
|
|
||
| async function buildServerState( |
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.
"build" -> "load" or "initialize" or something similar that makes it more obvious this shoudl be called only once?
otherwise feels like some mithril-style function that can be called frame after frame or something liek that
| // Loading | ||
| // ============================================================================= | ||
|
|
||
| async function loadExtension<T>( |
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.
loadModuleResource?
| module: string, | ||
| resourceType: 'macros' | 'sql_modules' | 'proto_descriptors', | ||
| schema: z.ZodSchema, | ||
| transformKey?: ( |
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.
honestly this function is so generic that it's hard to tell what's going on at this point.
if you reach the state where you have a common function where you have to pass a "transformator" maybe just create 3 functions, like loadModuleMacros, loadModuleSqlModules etc?
| 'ws://127.0.0.1:8037', // For the adb websocket server. | ||
| 'https://*.google-analytics.com', | ||
| 'https://*.googleapis.com', // For Google Cloud Storage fetches. | ||
| 'https:', // Allow all HTTPS for extension servers and analytics. |
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.
imho this shoudl work differently:
- From the settings you know which servers you have enabled, so you know the server URLs1, which you can add here.
- furthermore the server extension (Which you'll have to cache in localstorage) will define some futher extensions to the CSP you'll need to merge here
This is an initial implementaton of RFC-0005
(#3227).