|
| 1 | +# Preloading System |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +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. |
| 6 | + |
| 7 | +## Architecture |
| 8 | + |
| 9 | +The preloading system consists of several interconnected components: |
| 10 | + |
| 11 | +``` |
| 12 | ++---------------------------------------------------------------------+ |
| 13 | +| EditorService | |
| 14 | +| (Orchestrates dependency fetching and caching) | |
| 15 | ++----------------------------------+----------------------------------+ |
| 16 | + | |
| 17 | + +------------------+------------------+ |
| 18 | + | | | |
| 19 | + v v v |
| 20 | + +--------------------+ +--------------+ +--------------------+ |
| 21 | + | RESTAPIRepository | |EditorPreload | |EditorAssetLibrary | |
| 22 | + | (API caching) | | List | | (JS/CSS bundles) | |
| 23 | + +---------+----------+ +------+-------+ +--------------------+ |
| 24 | + | | |
| 25 | + v v |
| 26 | + +--------------------+ +-------------------------------------+ |
| 27 | + | EditorURLCache | | GBKitGlobal | |
| 28 | + | (Disk caching) | | (Serialized to window.GBKit) | |
| 29 | + +---------+----------+ +------------------+------------------+ |
| 30 | + | | |
| 31 | + v v |
| 32 | + +--------------------+ +-----------------------------+ |
| 33 | + |EditorCachePolicy | | JavaScript Preloading | |
| 34 | + | (TTL management) | | Middleware | |
| 35 | + +--------------------+ +-----------------------------+ |
| 36 | +``` |
| 37 | + |
| 38 | +## Key Components |
| 39 | + |
| 40 | +### EditorService |
| 41 | + |
| 42 | +The `EditorService` actor coordinates fetching all editor dependencies concurrently: |
| 43 | + |
| 44 | +**Swift** |
| 45 | +```swift |
| 46 | +let service = EditorService(configuration: config) |
| 47 | +let dependencies = try await service.prepare { progress in |
| 48 | + print("Loading: \(progress.fractionCompleted * 100)%") |
| 49 | +} |
| 50 | +``` |
| 51 | + |
| 52 | +**Kotlin** |
| 53 | +```kotlin |
| 54 | +// TBD |
| 55 | +``` |
| 56 | + |
| 57 | +The `prepare` method fetches these resources in parallel: |
| 58 | +- Editor settings (theme styles, block settings) |
| 59 | +- Asset bundles (JavaScript and CSS files) |
| 60 | +- Preload list (API responses for editor initialization) |
| 61 | + |
| 62 | +### EditorPreloadList |
| 63 | + |
| 64 | +The `EditorPreloadList` struct contains pre-fetched API responses that are serialized to JSON and injected into the editor's JavaScript runtime: |
| 65 | + |
| 66 | +| Property | API Endpoint | Description | |
| 67 | +|----------|--------------|-------------| |
| 68 | +| `postData` | `/wp/v2/posts/{id}?context=edit` | The post being edited (existing posts only) | |
| 69 | +| `postTypeData` | `/wp/v2/types/{type}?context=edit` | Schema for the current post type | |
| 70 | +| `postTypesData` | `/wp/v2/types?context=view` | All available post types | |
| 71 | +| `activeThemeData` | `/wp/v2/themes?context=edit&status=active` | Active theme information | |
| 72 | +| `settingsOptionsData` | `OPTIONS /wp/v2/settings` | Site settings schema | |
| 73 | + |
| 74 | +### EditorURLCache |
| 75 | + |
| 76 | +The `EditorURLCache` provides disk-based caching for API responses, keyed by URL and HTTP method. It supports three cache policies via `EditorCachePolicy`: |
| 77 | + |
| 78 | +| Policy | Behavior | |
| 79 | +|--------|----------| |
| 80 | +| `.ignore` | Never use cached responses (force fresh data) | |
| 81 | +| `.maxAge(TimeInterval)` | Use cached responses younger than the specified age | |
| 82 | +| `.always` | Always use cached responses regardless of age | |
| 83 | + |
| 84 | +Example: |
| 85 | + |
| 86 | +**Swift** |
| 87 | +```swift |
| 88 | +// Cache responses for up to 1 hour |
| 89 | +let service = EditorService( |
| 90 | + configuration: config, |
| 91 | + cachePolicy: .maxAge(3600) |
| 92 | +) |
| 93 | +``` |
| 94 | + |
| 95 | +**Kotlin** |
| 96 | +```kotlin |
| 97 | +// TBD |
| 98 | +``` |
| 99 | + |
| 100 | +### RESTAPIRepository |
| 101 | + |
| 102 | +The `RESTAPIRepository` handles fetching and caching individual API responses. It follows a read-through caching pattern: |
| 103 | + |
| 104 | +1. Check cache for existing response |
| 105 | +2. If cache hit and valid per policy, return cached data |
| 106 | +3. If cache miss or expired, fetch from network |
| 107 | +4. Store response in cache |
| 108 | +5. Return response |
| 109 | + |
| 110 | +## Data Flow |
| 111 | + |
| 112 | +### 1. Preparation Phase (Native) |
| 113 | + |
| 114 | +When `EditorService.prepare()` is called: |
| 115 | + |
| 116 | +``` |
| 117 | +EditorService.prepare() |
| 118 | + |-- prepareEditorSettings() -> EditorSettings |
| 119 | + |-- prepareAssetBundle() -> EditorAssetBundle |
| 120 | + +-- preparePreloadList() |
| 121 | + |-- prepareActiveTheme() -> EditorURLResponse |
| 122 | + |-- prepareSettingsOptions() -> EditorURLResponse |
| 123 | + |-- preparePost(type:) -> EditorURLResponse |
| 124 | + |-- preparePostTypes() -> EditorURLResponse |
| 125 | + +-- preparePost(id:) -> EditorURLResponse (if editing existing post) |
| 126 | +``` |
| 127 | + |
| 128 | +### 2. Serialization Phase (Native) |
| 129 | + |
| 130 | +The `EditorPreloadList` is converted to JSON via `build()`: |
| 131 | + |
| 132 | +```json |
| 133 | +{ |
| 134 | + "/wp/v2/types/post?context=edit": { |
| 135 | + "body": { "slug": "post", "supports": { ... } }, |
| 136 | + "headers": { "Link": "<...>; rel=\"https://api.w.org/\"" } |
| 137 | + }, |
| 138 | + "/wp/v2/types?context=view": { |
| 139 | + "body": { "post": { ... }, "page": { ... } }, |
| 140 | + "headers": {} |
| 141 | + }, |
| 142 | + "/wp/v2/themes?context=edit&status=active": { |
| 143 | + "body": [ ... ], |
| 144 | + "headers": {} |
| 145 | + }, |
| 146 | + "OPTIONS": { |
| 147 | + "/wp/v2/settings": { |
| 148 | + "body": { ... }, |
| 149 | + "headers": {} |
| 150 | + } |
| 151 | + } |
| 152 | +} |
| 153 | +``` |
| 154 | + |
| 155 | +### 3. Injection Phase (Native to Web) |
| 156 | + |
| 157 | +The `GBKitGlobal` struct packages all configuration and preload data, then injects it into the WebView as `window.GBKit`: |
| 158 | + |
| 159 | +```javascript |
| 160 | +window.GBKit = { |
| 161 | + siteURL: "https://example.com", |
| 162 | + siteApiRoot: "https://example.com/wp-json", |
| 163 | + authHeader: "Bearer ...", |
| 164 | + preloadData: { /* serialized EditorPreloadList */ }, |
| 165 | + editorSettings: { /* theme styles, colors, etc. */ }, |
| 166 | + // ... other configuration |
| 167 | +}; |
| 168 | +``` |
| 169 | + |
| 170 | +### 4. Consumption Phase (JavaScript) |
| 171 | + |
| 172 | +The `@wordpress/api-fetch` package includes a preloading middleware that intercepts API requests: |
| 173 | + |
| 174 | +```javascript |
| 175 | +// In src/utils/api-fetch.js |
| 176 | +export function configureApiFetch() { |
| 177 | + const { preloadData } = getGBKit(); |
| 178 | + |
| 179 | + apiFetch.use( |
| 180 | + apiFetch.createPreloadingMiddleware(preloadData ?? defaultPreloadData) |
| 181 | + ); |
| 182 | +} |
| 183 | +``` |
| 184 | +
|
| 185 | +When Gutenberg makes an API request: |
| 186 | +
|
| 187 | +1. The preloading middleware checks if the request path exists in `preloadData` |
| 188 | +2. If found, the cached response is returned immediately (no network request) |
| 189 | +3. If not found, the request proceeds to the network |
| 190 | +4. The preload entry is consumed (one-time use) to ensure fresh data on subsequent requests |
| 191 | +
|
| 192 | +## Header Filtering |
| 193 | +
|
| 194 | +Only certain headers are preserved in preload responses to match WordPress core's behavior: |
| 195 | +
|
| 196 | +- `Accept` - Content type negotiation |
| 197 | +- `Link` - REST API discovery and pagination |
| 198 | +
|
| 199 | +This filtering is performed by `EditorURLResponse.asPreloadResponse()`. |
| 200 | +
|
| 201 | +## Cache Management |
| 202 | +
|
| 203 | +### Automatic Cleanup |
| 204 | +
|
| 205 | +`EditorService` automatically cleans up old asset bundles once per day: |
| 206 | +
|
| 207 | +**Swift** |
| 208 | +```swift |
| 209 | +try await onceEvery(.seconds(86_400)) { |
| 210 | + try await self.cleanup() |
| 211 | +} |
| 212 | +``` |
| 213 | +
|
| 214 | +**Kotlin** |
| 215 | +```kotlin |
| 216 | +//tbd |
| 217 | +``` |
| 218 | +
|
| 219 | +### Manual Cache Control |
| 220 | +
|
| 221 | +**Swift** |
| 222 | +```swift |
| 223 | +// Clear unused resources (keeps most recent) |
| 224 | +try await service.cleanup() |
| 225 | + |
| 226 | +// Clear all resources (requires re-download) |
| 227 | +try await service.purge() |
| 228 | +``` |
| 229 | +
|
| 230 | +**Kotlin** |
| 231 | +```kotlin |
| 232 | +//tbd |
| 233 | +``` |
| 234 | +
|
| 235 | +## Offline Mode |
| 236 | +
|
| 237 | +When `EditorConfiguration.isOfflineModeEnabled` is `true`, the preloading system returns empty dependencies: |
| 238 | +
|
| 239 | +```swift |
| 240 | +if self.configuration.isOfflineModeEnabled { |
| 241 | + return EditorDependencies( |
| 242 | + editorSettings: .undefined, |
| 243 | + assetBundle: .empty, |
| 244 | + preloadList: nil |
| 245 | + ) |
| 246 | +} |
| 247 | +``` |
| 248 | +
|
| 249 | +Offline mode doesn't refer to reguar site that are offline – it's for when you're using GutenbergKit separately from a WordPress |
| 250 | +site (for instance, the bundled editor in the demo app, or you just want an editor without the WP integration). |
| 251 | +
|
| 252 | +The JavaScript side falls back to `defaultPreloadData` which contains minimal type definitions to allow basic editor functionality. |
| 253 | +
|
| 254 | +## Progress Reporting |
| 255 | +
|
| 256 | +The preloading system reports its progress to give the user high-quality feedback about the loading process - if the user loads the |
| 257 | +editor without `EditorDependencies` present, the editor will display a loading screen with a progress bar. If the user provides `EditorDependencies` |
| 258 | +that contain everything the editor needs, the progress bar will never be displayed. |
| 259 | +
|
| 260 | +## EditorDependencies |
| 261 | +
|
| 262 | +`EditorDependencies` contains all pre-fetched resources needed to initialize the editor instantly. |
| 263 | +
|
| 264 | +| Property | Type | Description | |
| 265 | +|----------|------|-------------| |
| 266 | +| `editorSettings` | `EditorSettings` | Theme styles, colors, typography, block settings | |
| 267 | +| `assetBundle` | `EditorAssetBundle` | Cached JavaScript/CSS for plugins/themes | |
| 268 | +| `preloadList` | `EditorPreloadList?` | Pre-fetched API responses | |
| 269 | +
|
| 270 | +### Obtaining Dependencies |
| 271 | +
|
| 272 | +```swift |
| 273 | +let service = EditorService(configuration: configuration) |
| 274 | +let dependencies = try await service.prepare { progress in |
| 275 | + loadingView.progress = progress.fractionCompleted |
| 276 | +} |
| 277 | +``` |
| 278 | +
|
| 279 | +### EditorViewController Loading Flows |
| 280 | +
|
| 281 | +`EditorViewController` supports two loading flows based on whether dependencies are provided: |
| 282 | +
|
| 283 | +#### Flow 1: Dependencies Provided (Recommended) |
| 284 | +
|
| 285 | +```swift |
| 286 | +let editor = EditorViewController( |
| 287 | + configuration: configuration, |
| 288 | + dependencies: dependencies // Loads immediately |
| 289 | +) |
| 290 | +``` |
| 291 | +
|
| 292 | +The editor skips the progress UI and loads the WebView immediately. |
| 293 | +
|
| 294 | +#### Flow 2: No Dependencies (Fallback) |
| 295 | +
|
| 296 | +```swift |
| 297 | +let editor = EditorViewController( |
| 298 | + configuration: configuration |
| 299 | + // No dependencies - fetches automatically |
| 300 | +) |
| 301 | +``` |
| 302 | +
|
| 303 | +The editor displays a progress bar while fetching, then loads once complete. |
| 304 | +
|
| 305 | +### Best Practice: Prepare Early |
| 306 | +
|
| 307 | +Fetch dependencies before the user needs the editor: |
| 308 | +
|
| 309 | +```swift |
| 310 | +class PostListViewController: UIViewController { |
| 311 | + private var editorDependencies: EditorDependencies? |
| 312 | + private let editorService: EditorService |
| 313 | + |
| 314 | + override func viewDidLoad() { |
| 315 | + super.viewDidLoad() |
| 316 | + Task { |
| 317 | + self.editorDependencies = try? await editorService.prepare { _ in } |
| 318 | + } |
| 319 | + } |
| 320 | + |
| 321 | + func editPost(_ post: Post) { |
| 322 | + let editor = EditorViewController( |
| 323 | + configuration: EditorConfiguration(post: post), |
| 324 | + dependencies: editorDependencies |
| 325 | + ) |
| 326 | + navigationController?.pushViewController(editor, animated: true) |
| 327 | + } |
| 328 | +} |
0 commit comments