Skip to content

Commit f2214fb

Browse files
authored
Release 1.12.2 (#79) (#80)
1 parent 4ea9608 commit f2214fb

13 files changed

Lines changed: 495 additions & 129 deletions

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
### 1.12.2
6+
7+
#### Changed
8+
9+
- Replaced row-level freshness checks with a revision-based snapshot protocol for IndexedDB cache hydration
10+
- Added a dedicated JS catalog boundary so startup no longer coordinates cache invalidation and REST refill directly
11+
12+
#### Fixed
13+
14+
- REST responses now echo the cache revision so hydration can detect cross-runtime mismatches during batched loads
15+
16+
#### Added
17+
18+
- Added boundary-focused tests covering cache reuse, stale refresh, hydration failures, and revision mismatches
19+
520
### 1.12.1
621

722
#### Fixed

README.md

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -156,25 +156,25 @@ Try it in [WordPress Playground](https://playground.wordpress.net/?blueprint-url
156156
```
157157
PHP (server) JS (browser)
158158
───────────── ────────────
159-
get_timestamp() ──inline_script──▶ pluginAllSitesMenu.timestamp
160-
161-
Compare with IndexedDB timestamp
162-
163-
┌──────────┴──────────┐
164-
│ mismatch │ match
165-
▼ ▼
166-
Clear DB Use cached data
167-
│ │
168-
▼ │
169-
REST /sites?offset=0 │
170-
REST /sites?offset=100 │
171-
…(batched) │
172-
│ │
173-
▼ ▼
174-
Store in IndexedDB ──▶ Render menu
159+
get_revision() ──inline_script──▶ pluginAllSitesMenu.revision
160+
161+
Compare with snapshot metadata revision
162+
163+
┌──────────┴──────────┐
164+
│ mismatch │ match
165+
▼ ▼
166+
Clear DB Use cached snapshot
167+
│ │
168+
▼ │
169+
REST /sites?offset=0 │
170+
REST /sites?offset=100 │
171+
…(batched) │
172+
│ │
173+
▼ ▼
174+
Store in IndexedDB ──▶ Render menu
175175
```
176176

177-
The PHP timestamp acts as a cache version. It is bumped whenever a site is added/deleted, a blog name changes, or a monitored plugin is (de)activated. On the client side, a mismatch triggers a full re-fetch; a match means the cached data is used as-is.
177+
The PHP revision acts as an opaque cache version. It is bumped whenever a site is added/deleted, a blog name changes, or a monitored plugin is (de)activated. On the client side, a mismatch triggers a full re-fetch; a match means the cached snapshot is used as-is.
178178

179179
<details>
180180
<summary>IndexedDB storage</summary>

build/index.asset.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<?php return array('dependencies' => array('wp-api-fetch', 'wp-i18n'), 'version' => '1e33e31af10a9a12d92d');
1+
<?php return array('dependencies' => array('wp-api-fetch', 'wp-i18n'), 'version' => 'da739b20a95bf519c7d8');

build/index.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "soderlind/super-admin-all-sites-menu",
33
"description": "For the super admin, replace WP Admin Bar My Sites menu with an All Sites menu.",
4-
"version": "1.12.1",
4+
"version": "1.12.2",
55
"keywords": [
66
"wordpress",
77
"multisite",

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "super-admin-all-sites-menu",
3-
"version": "1.12.1",
3+
"version": "1.12.2",
44
"description": "For the super admin, replace WP Admin Bar My Sites menu with an All Sites menu.",
55
"main": "index.js",
66
"scripts": {

readme.txt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
=== Super Admin All Sites Menu ===
2-
Stable tag: 1.12.1
2+
Stable tag: 1.12.2
33
Requires at least: 5.6
44
Tested up to: 7.0
55
Requires PHP: 8.0
@@ -121,6 +121,12 @@ You can use the following filters to override the defaults:
121121

122122
== Changelog ==
123123

124+
= 1.12.2 =
125+
* Changed: Replaced row-level freshness checks with a revision-based snapshot protocol for IndexedDB cache hydration
126+
* Changed: Added a dedicated JS catalog boundary so startup no longer coordinates cache invalidation and REST refill directly
127+
* Fixed: REST responses now echo the cache revision so hydration can detect cross-runtime mismatches during batched loads
128+
* Added: Boundary-focused tests covering cache reuse, stale refresh, hydration failures, and revision mismatches
129+
124130
= 1.12.1 =
125131
* Fixed: Menu sometimes not loading after a REST API error — partial data is cleared and the next page load retries automatically
126132
* Fixed: Nonce middleware stacking on every REST batch request, causing slowdowns on large networks

src/index.js

Lines changed: 20 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@
77
*/
88

99
import { addSearch } from './modules/search.js';
10-
import { IndexedDB } from './modules/db.js';
11-
import { loadSites } from './modules/rest.js';
10+
import {
11+
createAllSitesCatalog,
12+
createDexieSnapshotStore,
13+
} from './modules/catalog.js';
14+
import { createRemoteCatalogPort } from './modules/rest.js';
1215
import { observeContainer, observeMenuHeight } from './modules/observe.js';
1316
import { refreshAdminbar } from './modules/refresh.js';
1417
import { siteMenu } from './modules/menu.js';
@@ -20,11 +23,6 @@ import { siteMenu } from './modules/menu.js';
2023
const SETTINGS = {
2124
DB_NAME: 'allsites',
2225
TABLE_NAME: 'sites',
23-
DB_VERSIONS: [
24-
'id,name,url',
25-
'id,name,url,timestamp',
26-
'id,name,url,timestamp,blog_id',
27-
],
2826
};
2927

3028
/**
@@ -46,73 +44,39 @@ async function init() {
4644
if ( ! elements.load || ! elements.menu ) return;
4745

4846
try {
49-
const db = new IndexedDB(
50-
SETTINGS.DB_NAME,
51-
SETTINGS.TABLE_NAME,
52-
SETTINGS.DB_VERSIONS
53-
);
47+
const catalog = createAllSitesCatalog( {
48+
remote: createRemoteCatalogPort(),
49+
store: createDexieSnapshotStore( {
50+
dbName: SETTINGS.DB_NAME,
51+
sitesTable: SETTINGS.TABLE_NAME,
52+
} ),
53+
} );
5454

5555
if ( pluginAllSitesMenu.displaySearch ) {
5656
addSearch();
5757
}
5858

5959
observeMenuHeight( elements.menu );
60-
await populateDB( db );
61-
setupLoadMoreObserver( elements.load, db );
60+
await catalog.boot( {
61+
revision: pluginAllSitesMenu.revision,
62+
pageSize: pluginAllSitesMenu.loadincrements,
63+
} );
64+
setupLoadMoreObserver( elements.load, catalog );
6265
} catch ( error ) {
6366
console.error( 'Initialization failed:', error );
6467
}
6568
}
6669

6770
document.addEventListener( 'DOMContentLoaded', init );
6871

69-
/**
70-
* Populate the database with sites.
71-
*
72-
* @author Per Søderlind
73-
* @param {IndexedDB} db
74-
* @param {object} el
75-
*/
76-
77-
/**
78-
* Sets up database population and handles timestamp verification
79-
*
80-
* @async
81-
* @param {IndexedDB} db - Database instance to populate
82-
* @throws {Error} When database operations fail
83-
*/
84-
async function populateDB( db ) {
85-
const data = await db.getFirstRow();
86-
if (
87-
typeof data !== 'undefined' &&
88-
typeof data.timestamp !== 'undefined' &&
89-
pluginAllSitesMenu.timestamp > data.timestamp
90-
) {
91-
await db.delete();
92-
}
93-
94-
if ( ( await db.count() ) === 0 ) {
95-
try {
96-
await loadSites( db, {
97-
offset: 0,
98-
delayMs: 200,
99-
} );
100-
} catch ( err ) {
101-
// Clear partial data so next page load retries from scratch.
102-
await db.delete();
103-
throw err;
104-
}
105-
}
106-
}
107-
10872
/**
10973
* Configures the intersection observer for loading more sites
11074
*
11175
* @param {HTMLElement} loadElement - The load more trigger element
112-
* @param {IndexedDB} db - Database instance containing sites
76+
* @param {{list: (orderBy?: string) => Promise<Array>}} catalog - Catalog boundary containing sites
11377
* @returns {IntersectionObserver} The configured observer
11478
*/
115-
function setupLoadMoreObserver( loadElement, db ) {
79+
function setupLoadMoreObserver( loadElement, catalog ) {
11680
let loaded = false;
11781
const observedLoadMore = observeContainer( loadElement, async () => {
11882
if ( loaded ) {
@@ -124,7 +88,7 @@ function setupLoadMoreObserver( loadElement, db ) {
12488
pluginAllSitesMenu.orderBy === 'id'
12589
? 'blog_id'
12690
: pluginAllSitesMenu.orderBy;
127-
const sites = await db.read( orderBy );
91+
const sites = await catalog.list( orderBy );
12892
const sitesMenu = sites.reduce( ( acc, site ) => {
12993
return acc + siteMenu( site );
13094
}, '' );

src/modules/catalog.js

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { Dexie } from 'dexie';
2+
3+
/**
4+
* Create the runtime catalog boundary for site cache freshness and reads.
5+
*
6+
* @param {Object} deps Catalog dependencies
7+
* @param {Object} deps.remote Remote adapter
8+
* @param {Object} deps.store Snapshot store adapter
9+
* @returns {{boot: (manifest: {revision: string, pageSize?: number}) => Promise<{revision: string, source: string, count: number}>, list: (orderBy?: string) => Promise<Array>}}
10+
*/
11+
export function createAllSitesCatalog( { remote, store } ) {
12+
let bootPromise;
13+
14+
return {
15+
async boot( manifest ) {
16+
if ( ! manifest?.revision ) {
17+
throw new Error( 'Catalog boot requires a revision' );
18+
}
19+
20+
if ( ! bootPromise ) {
21+
bootPromise = ensureFreshSnapshot(
22+
manifest,
23+
remote,
24+
store
25+
).finally( () => {
26+
bootPromise = undefined;
27+
} );
28+
}
29+
30+
return bootPromise;
31+
},
32+
33+
list( orderBy = 'name' ) {
34+
return store.readAll( orderBy );
35+
},
36+
};
37+
}
38+
39+
async function ensureFreshSnapshot( manifest, remote, store ) {
40+
const snapshot = await store.getSnapshotMeta();
41+
if ( snapshot?.revision === manifest.revision ) {
42+
return {
43+
revision: snapshot.revision,
44+
source: 'cache',
45+
count: snapshot.count,
46+
};
47+
}
48+
49+
await store.clear();
50+
51+
const items = await fetchSnapshot( manifest, remote );
52+
await store.replaceSnapshot( {
53+
revision: manifest.revision,
54+
items,
55+
} );
56+
57+
return {
58+
revision: manifest.revision,
59+
source: 'network',
60+
count: items.length,
61+
};
62+
}
63+
64+
async function fetchSnapshot( manifest, remote ) {
65+
const items = [];
66+
let offset = 0;
67+
const pageSize = Number( manifest.pageSize ) || 100;
68+
69+
while ( true ) {
70+
const page = await remote.fetchPage( {
71+
offset,
72+
limit: pageSize,
73+
expectedRevision: manifest.revision,
74+
} );
75+
76+
if ( page.revision !== manifest.revision ) {
77+
throw new Error( 'Catalog revision changed during hydration' );
78+
}
79+
80+
if ( page.items.length > 0 ) {
81+
items.push( ...page.items );
82+
}
83+
84+
if ( page.done ) {
85+
return items;
86+
}
87+
88+
offset += pageSize;
89+
}
90+
}
91+
92+
/**
93+
* Create the production Dexie-backed store for site snapshots.
94+
*
95+
* @param {Object} options Store configuration
96+
* @param {string} options.dbName Database name
97+
* @param {string} options.sitesTable Sites table name
98+
* @returns {{getSnapshotMeta: () => Promise<{revision: string, count: number}|null>, clear: () => Promise<void>, replaceSnapshot: (input: {revision: string, items: Array}) => Promise<void>, readAll: (orderBy?: string) => Promise<Array>}}
99+
*/
100+
export function createDexieSnapshotStore( { dbName, sitesTable } ) {
101+
const db = new Dexie( dbName );
102+
db.version( 1 ).stores( {
103+
[ sitesTable ]: 'id,name,url',
104+
} );
105+
db.version( 2 ).stores( {
106+
[ sitesTable ]: 'id,name,url,timestamp',
107+
} );
108+
db.version( 3 ).stores( {
109+
[ sitesTable ]: 'id,name,url,timestamp,blog_id',
110+
} );
111+
db.version( 4 ).stores( {
112+
[ sitesTable ]: 'id,name,url,blog_id',
113+
catalog_meta: 'key',
114+
} );
115+
116+
const metaTable = db.table( 'catalog_meta' );
117+
const siteTable = db.table( sitesTable );
118+
119+
return {
120+
async getSnapshotMeta() {
121+
const meta = await metaTable.get( 'snapshot' );
122+
if ( ! meta?.revision ) {
123+
return null;
124+
}
125+
126+
return {
127+
revision: meta.revision,
128+
count: Number( meta.count ) || 0,
129+
};
130+
},
131+
132+
async clear() {
133+
await db.transaction( 'rw', siteTable, metaTable, async () => {
134+
await siteTable.clear();
135+
await metaTable.delete( 'snapshot' );
136+
} );
137+
},
138+
139+
async replaceSnapshot( { revision, items } ) {
140+
await db.transaction( 'rw', siteTable, metaTable, async () => {
141+
await siteTable.clear();
142+
if ( items.length > 0 ) {
143+
await siteTable.bulkPut( items );
144+
}
145+
await metaTable.put( {
146+
key: 'snapshot',
147+
revision,
148+
count: items.length,
149+
} );
150+
} );
151+
},
152+
153+
async readAll( orderBy = 'name' ) {
154+
return siteTable.orderBy( orderBy ).toArray();
155+
},
156+
};
157+
}

0 commit comments

Comments
 (0)