-
Notifications
You must be signed in to change notification settings - Fork 3
Add iOS Preload List #250
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
Merged
Merged
Add iOS Preload List #250
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
50f1b1d
Add Preloader Logic
jkmassel b8d37fd
Update GutenbergKit for the demo app
jkmassel 1aca406
Add site preparation screen
jkmassel 7fb256b
Add documentation
jkmassel efb4ed0
Remove old tests
jkmassel c95bc89
Fix rebase issue
jkmassel 6aa25bc
fix visibility issue
jkmassel b1b9a85
Address HTTPClient feedback
jkmassel c8d4f11
Remove EditorKeyValueCache
jkmassel 1d53407
Apply suggestions from code review
jkmassel 5cdbc5b
Remove `loadEditorTask` synthesizing accessor
jkmassel f2dcb1c
Update ios/Sources/GutenbergKit/Sources/EditorLogging.swift
jkmassel 48e3640
Update ios/Demo-iOS/Sources/Views/SitePreparationView.swift
jkmassel ddfe7c9
Pass cache policy into EditorAssetLibrary
jkmassel 46b6838
Move editor start button
jkmassel 50800e0
Use modal editor
jkmassel 354d8f9
Remove editor loading state machine
jkmassel 935fd8a
Drop `EditorErrorViewController`
jkmassel 5682570
Use EditorLocalization
jkmassel 39cff26
Fix a potential bug with editor loading
jkmassel 900dce7
Update ios/Demo-iOS/Sources/Views/SitePreparationView.swift
jkmassel 304e4fb
Update ios/Demo-iOS/Sources/Views/EditorView.swift
jkmassel e2a578a
Update ios/Demo-iOS/Sources/Views/EditorView.swift
jkmassel 9fba044
Rename to Foundation+Extensions.swift
jkmassel 7752bd5
Inline cachePolicy usage docs
jkmassel File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,328 @@ | ||
| # Preloading System | ||
|
|
||
| ## Overview | ||
|
|
||
| The preloading system in GutenbergKit pre-fetches WordPress REST API responses before the editor loads, eliminating network latency during editor initialization. By injecting cached API responses directly into the JavaScript runtime, the Gutenberg editor can almost always initialize instantly without waiting for network requests. | ||
|
|
||
| ## Architecture | ||
|
|
||
| The preloading system consists of several interconnected components: | ||
|
|
||
| ``` | ||
| +---------------------------------------------------------------------+ | ||
| | EditorService | | ||
| | (Orchestrates dependency fetching and caching) | | ||
| +----------------------------------+----------------------------------+ | ||
| | | ||
| +------------------+------------------+ | ||
| | | | | ||
| v v v | ||
| +--------------------+ +--------------+ +--------------------+ | ||
| | RESTAPIRepository | |EditorPreload | |EditorAssetLibrary | | ||
| | (API caching) | | List | | (JS/CSS bundles) | | ||
| +---------+----------+ +------+-------+ +--------------------+ | ||
| | | | ||
| v v | ||
| +--------------------+ +-------------------------------------+ | ||
| | EditorURLCache | | GBKitGlobal | | ||
| | (Disk caching) | | (Serialized to window.GBKit) | | ||
| +---------+----------+ +------------------+------------------+ | ||
| | | | ||
| v v | ||
| +--------------------+ +-----------------------------+ | ||
| |EditorCachePolicy | | JavaScript Preloading | | ||
| | (TTL management) | | Middleware | | ||
| +--------------------+ +-----------------------------+ | ||
| ``` | ||
|
|
||
| ## Key Components | ||
|
|
||
| ### EditorService | ||
|
|
||
| The `EditorService` actor coordinates fetching all editor dependencies concurrently: | ||
|
|
||
| **Swift** | ||
| ```swift | ||
| let service = EditorService(configuration: config) | ||
| let dependencies = try await service.prepare { progress in | ||
| print("Loading: \(progress.fractionCompleted * 100)%") | ||
| } | ||
| ``` | ||
|
|
||
| **Kotlin** | ||
| ```kotlin | ||
| // TBD | ||
| ``` | ||
|
|
||
| The `prepare` method fetches these resources in parallel: | ||
| - Editor settings (theme styles, block settings) | ||
| - Asset bundles (JavaScript and CSS files) | ||
| - Preload list (API responses for editor initialization) | ||
|
|
||
| ### EditorPreloadList | ||
|
|
||
| The `EditorPreloadList` struct contains pre-fetched API responses that are serialized to JSON and injected into the editor's JavaScript runtime: | ||
|
|
||
| | Property | API Endpoint | Description | | ||
| |----------|--------------|-------------| | ||
| | `postData` | `/wp/v2/posts/{id}?context=edit` | The post being edited (existing posts only) | | ||
| | `postTypeData` | `/wp/v2/types/{type}?context=edit` | Schema for the current post type | | ||
| | `postTypesData` | `/wp/v2/types?context=view` | All available post types | | ||
| | `activeThemeData` | `/wp/v2/themes?context=edit&status=active` | Active theme information | | ||
| | `settingsOptionsData` | `OPTIONS /wp/v2/settings` | Site settings schema | | ||
|
|
||
| ### EditorURLCache | ||
|
|
||
| The `EditorURLCache` provides disk-based caching for API responses, keyed by URL and HTTP method. It supports three cache policies via `EditorCachePolicy`: | ||
|
|
||
| | Policy | Behavior | | ||
| |--------|----------| | ||
| | `.ignore` | Never use cached responses (force fresh data) | | ||
| | `.maxAge(TimeInterval)` | Use cached responses younger than the specified age | | ||
| | `.always` | Always use cached responses regardless of age | | ||
|
|
||
| Example: | ||
|
|
||
| **Swift** | ||
| ```swift | ||
| // Cache responses for up to 1 hour | ||
| let service = EditorService( | ||
| configuration: config, | ||
| cachePolicy: .maxAge(3600) | ||
| ) | ||
| ``` | ||
|
|
||
| **Kotlin** | ||
| ```kotlin | ||
| // TBD | ||
| ``` | ||
|
|
||
| ### RESTAPIRepository | ||
|
|
||
| The `RESTAPIRepository` handles fetching and caching individual API responses. It follows a read-through caching pattern: | ||
|
|
||
| 1. Check cache for existing response | ||
| 2. If cache hit and valid per policy, return cached data | ||
| 3. If cache miss or expired, fetch from network | ||
| 4. Store response in cache | ||
| 5. Return response | ||
|
|
||
| ## Data Flow | ||
|
|
||
| ### 1. Preparation Phase (Native) | ||
|
|
||
| When `EditorService.prepare()` is called: | ||
|
|
||
| ``` | ||
| EditorService.prepare() | ||
| |-- prepareEditorSettings() -> EditorSettings | ||
| |-- prepareAssetBundle() -> EditorAssetBundle | ||
| +-- preparePreloadList() | ||
| |-- prepareActiveTheme() -> EditorURLResponse | ||
| |-- prepareSettingsOptions() -> EditorURLResponse | ||
| |-- preparePost(type:) -> EditorURLResponse | ||
| |-- preparePostTypes() -> EditorURLResponse | ||
| +-- preparePost(id:) -> EditorURLResponse (if editing existing post) | ||
| ``` | ||
|
|
||
| ### 2. Serialization Phase (Native) | ||
|
|
||
| The `EditorPreloadList` is converted to JSON via `build()`: | ||
|
|
||
| ```json | ||
| { | ||
| "/wp/v2/types/post?context=edit": { | ||
| "body": { "slug": "post", "supports": { ... } }, | ||
| "headers": { "Link": "<...>; rel=\"https://api.w.org/\"" } | ||
| }, | ||
| "/wp/v2/types?context=view": { | ||
| "body": { "post": { ... }, "page": { ... } }, | ||
| "headers": {} | ||
| }, | ||
| "/wp/v2/themes?context=edit&status=active": { | ||
| "body": [ ... ], | ||
| "headers": {} | ||
| }, | ||
| "OPTIONS": { | ||
| "/wp/v2/settings": { | ||
| "body": { ... }, | ||
| "headers": {} | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### 3. Injection Phase (Native to Web) | ||
|
|
||
| The `GBKitGlobal` struct packages all configuration and preload data, then injects it into the WebView as `window.GBKit`: | ||
|
|
||
| ```javascript | ||
| window.GBKit = { | ||
| siteURL: "https://example.com", | ||
| siteApiRoot: "https://example.com/wp-json", | ||
| authHeader: "Bearer ...", | ||
| preloadData: { /* serialized EditorPreloadList */ }, | ||
| editorSettings: { /* theme styles, colors, etc. */ }, | ||
| // ... other configuration | ||
| }; | ||
| ``` | ||
|
|
||
| ### 4. Consumption Phase (JavaScript) | ||
|
|
||
| The `@wordpress/api-fetch` package includes a preloading middleware that intercepts API requests: | ||
|
|
||
| ```javascript | ||
| // In src/utils/api-fetch.js | ||
| export function configureApiFetch() { | ||
| const { preloadData } = getGBKit(); | ||
|
|
||
| apiFetch.use( | ||
| apiFetch.createPreloadingMiddleware(preloadData ?? defaultPreloadData) | ||
| ); | ||
| } | ||
| ``` | ||
|
|
||
| When Gutenberg makes an API request: | ||
|
|
||
| 1. The preloading middleware checks if the request path exists in `preloadData` | ||
| 2. If found, the cached response is returned immediately (no network request) | ||
| 3. If not found, the request proceeds to the network | ||
| 4. The preload entry is consumed (one-time use) to ensure fresh data on subsequent requests | ||
|
|
||
| ## Header Filtering | ||
|
|
||
| Only certain headers are preserved in preload responses to match WordPress core's behavior: | ||
|
|
||
| - `Accept` - Content type negotiation | ||
| - `Link` - REST API discovery and pagination | ||
|
|
||
| This filtering is performed by `EditorURLResponse.asPreloadResponse()`. | ||
|
|
||
| ## Cache Management | ||
|
|
||
| ### Automatic Cleanup | ||
|
|
||
| `EditorService` automatically cleans up old asset bundles once per day: | ||
|
|
||
| **Swift** | ||
| ```swift | ||
| try await onceEvery(.seconds(86_400)) { | ||
| try await self.cleanup() | ||
| } | ||
| ``` | ||
|
|
||
| **Kotlin** | ||
| ```kotlin | ||
| //tbd | ||
| ``` | ||
|
|
||
| ### Manual Cache Control | ||
|
|
||
| **Swift** | ||
| ```swift | ||
| // Clear unused resources (keeps most recent) | ||
| try await service.cleanup() | ||
|
|
||
| // Clear all resources (requires re-download) | ||
| try await service.purge() | ||
| ``` | ||
|
|
||
| **Kotlin** | ||
| ```kotlin | ||
| //tbd | ||
| ``` | ||
|
|
||
| ## Offline Mode | ||
|
|
||
| When `EditorConfiguration.isOfflineModeEnabled` is `true`, the preloading system returns empty dependencies: | ||
|
|
||
| ```swift | ||
| if self.configuration.isOfflineModeEnabled { | ||
| return EditorDependencies( | ||
| editorSettings: .undefined, | ||
| assetBundle: .empty, | ||
| preloadList: nil | ||
| ) | ||
| } | ||
| ``` | ||
|
|
||
| Offline mode doesn't refer to reguar site that are offline – it's for when you're using GutenbergKit separately from a WordPress | ||
| site (for instance, the bundled editor in the demo app, or you just want an editor without the WP integration). | ||
|
|
||
| The JavaScript side falls back to `defaultPreloadData` which contains minimal type definitions to allow basic editor functionality. | ||
|
|
||
| ## Progress Reporting | ||
|
|
||
| The preloading system reports its progress to give the user high-quality feedback about the loading process - if the user loads the | ||
| editor without `EditorDependencies` present, the editor will display a loading screen with a progress bar. If the user provides `EditorDependencies` | ||
| that contain everything the editor needs, the progress bar will never be displayed. | ||
|
|
||
| ## EditorDependencies | ||
|
|
||
| `EditorDependencies` contains all pre-fetched resources needed to initialize the editor instantly. | ||
|
|
||
| | Property | Type | Description | | ||
| |----------|------|-------------| | ||
| | `editorSettings` | `EditorSettings` | Theme styles, colors, typography, block settings | | ||
| | `assetBundle` | `EditorAssetBundle` | Cached JavaScript/CSS for plugins/themes | | ||
| | `preloadList` | `EditorPreloadList?` | Pre-fetched API responses | | ||
|
|
||
| ### Obtaining Dependencies | ||
|
|
||
| ```swift | ||
| let service = EditorService(configuration: configuration) | ||
| let dependencies = try await service.prepare { progress in | ||
| loadingView.progress = progress.fractionCompleted | ||
| } | ||
| ``` | ||
|
|
||
| ### EditorViewController Loading Flows | ||
|
|
||
| `EditorViewController` supports two loading flows based on whether dependencies are provided: | ||
|
|
||
| #### Flow 1: Dependencies Provided (Recommended) | ||
|
|
||
| ```swift | ||
| let editor = EditorViewController( | ||
| configuration: configuration, | ||
| dependencies: dependencies // Loads immediately | ||
| ) | ||
| ``` | ||
|
|
||
| The editor skips the progress UI and loads the WebView immediately. | ||
|
|
||
| #### Flow 2: No Dependencies (Fallback) | ||
|
|
||
| ```swift | ||
| let editor = EditorViewController( | ||
| configuration: configuration | ||
| // No dependencies - fetches automatically | ||
| ) | ||
| ``` | ||
|
|
||
| The editor displays a progress bar while fetching, then loads once complete. | ||
|
|
||
| ### Best Practice: Prepare Early | ||
|
|
||
| Fetch dependencies before the user needs the editor: | ||
|
|
||
| ```swift | ||
| class PostListViewController: UIViewController { | ||
| private var editorDependencies: EditorDependencies? | ||
| private let editorService: EditorService | ||
|
|
||
| override func viewDidLoad() { | ||
| super.viewDidLoad() | ||
| Task { | ||
| self.editorDependencies = try? await editorService.prepare { _ in } | ||
| } | ||
| } | ||
|
|
||
| func editPost(_ post: Post) { | ||
| let editor = EditorViewController( | ||
| configuration: EditorConfiguration(post: post), | ||
| dependencies: editorDependencies | ||
| ) | ||
| navigationController?.pushViewController(editor, animated: true) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
3 changes: 2 additions & 1 deletion
3
ios/Demo-iOS/Gutenberg.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
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.
(nit) I would suggest to avoid committing documentation as it can be generated on the fly. The committed docs will likely get outdated quickly.
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't say I agree here – I don't think someone should have to say "hey bot, please generate some docs for me" if they're trying to understand how the system works?
Any bots working on the codebase can/should also reference this to know how it's supposed to work.
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 believe there is value in the documentation. I think the key is finding the right level of detail; the more detailed the static documentation, the more likely it will become outdated (or pose an unwieldy maintenance burden).
We might consider: