@oxyhq/core 18.0.0 → 19.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/i18n/locales/en-US.json +48 -5
- package/dist/cjs/i18n/locales/es-ES.json +48 -5
- package/dist/cjs/i18n/locales/locales/en-US.json +48 -5
- package/dist/cjs/i18n/locales/locales/es-ES.json +48 -5
- package/dist/cjs/index.js +10 -6
- package/dist/cjs/mixins/OxyServices.accounts.js +27 -2
- package/dist/cjs/mixins/OxyServices.user.js +14 -20
- package/dist/cjs/server/index.js +8 -2
- package/dist/cjs/server/userInvalidation.js +6 -28
- package/dist/cjs/utils/identityCacheSweep.js +97 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/i18n/locales/en-US.json +48 -5
- package/dist/esm/i18n/locales/es-ES.json +48 -5
- package/dist/esm/i18n/locales/locales/en-US.json +48 -5
- package/dist/esm/i18n/locales/locales/es-ES.json +48 -5
- package/dist/esm/index.js +1 -1
- package/dist/esm/mixins/OxyServices.accounts.js +22 -1
- package/dist/esm/mixins/OxyServices.user.js +14 -20
- package/dist/esm/server/index.js +5 -1
- package/dist/esm/server/userInvalidation.js +5 -26
- package/dist/esm/utils/identityCacheSweep.js +92 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/mixins/OxyServices.accounts.d.ts +39 -7
- package/dist/types/mixins/OxyServices.user.d.ts +9 -7
- package/dist/types/models/interfaces.d.ts +11 -3
- package/dist/types/server/index.d.ts +4 -2
- package/dist/types/server/userInvalidation.d.ts +5 -24
- package/dist/types/utils/identityCacheSweep.d.ts +80 -0
- package/package.json +2 -2
- package/src/i18n/locales/en-US.json +48 -5
- package/src/i18n/locales/es-ES.json +48 -5
- package/src/index.ts +8 -2
- package/src/mixins/OxyServices.accounts.ts +58 -7
- package/src/mixins/OxyServices.user.ts +14 -20
- package/src/mixins/__tests__/identityWriteCacheInvalidation.test.ts +370 -0
- package/src/models/interfaces.ts +11 -3
- package/src/server/__tests__/userInvalidation.test.ts +3 -20
- package/src/server/index.ts +5 -2
- package/src/server/userInvalidation.ts +8 -36
- package/src/utils/__tests__/identityCacheSweep.test.ts +151 -0
- package/src/utils/identityCacheSweep.ts +104 -0
|
@@ -1748,11 +1748,53 @@
|
|
|
1748
1748
|
"description": "A programmatic account with service credentials"
|
|
1749
1749
|
}
|
|
1750
1750
|
},
|
|
1751
|
-
"
|
|
1751
|
+
"accountCategory": {
|
|
1752
|
+
"news": "News",
|
|
1753
|
+
"politics": "Politics & policy",
|
|
1754
|
+
"business": "Business",
|
|
1755
|
+
"startup": "Startups & entrepreneurship",
|
|
1756
|
+
"finance": "Finance & investing",
|
|
1757
|
+
"crypto": "Crypto & web3",
|
|
1758
|
+
"marketplace": "Marketplace & classifieds",
|
|
1759
|
+
"retail": "Retail & ecommerce",
|
|
1760
|
+
"real_estate": "Real estate",
|
|
1752
1761
|
"agency": "Real estate agency",
|
|
1762
|
+
"landlord": "Landlord & property management",
|
|
1753
1763
|
"cooperative": "Housing cooperative",
|
|
1754
|
-
"
|
|
1755
|
-
"
|
|
1764
|
+
"architecture": "Architecture & construction",
|
|
1765
|
+
"technology": "Technology",
|
|
1766
|
+
"software": "Software & development",
|
|
1767
|
+
"ai": "AI & machine learning",
|
|
1768
|
+
"security": "Security & privacy",
|
|
1769
|
+
"automation": "Automation & tools",
|
|
1770
|
+
"science": "Science & research",
|
|
1771
|
+
"education": "Education & training",
|
|
1772
|
+
"books": "Books & writing",
|
|
1773
|
+
"health": "Health & medicine",
|
|
1774
|
+
"fitness": "Fitness & wellbeing",
|
|
1775
|
+
"sports": "Sports",
|
|
1776
|
+
"gaming": "Gaming & esports",
|
|
1777
|
+
"music": "Music",
|
|
1778
|
+
"film": "Film & TV",
|
|
1779
|
+
"podcast": "Podcasts & audio",
|
|
1780
|
+
"art": "Art & design",
|
|
1781
|
+
"photography": "Photography",
|
|
1782
|
+
"comedy": "Comedy",
|
|
1783
|
+
"food": "Food & drink",
|
|
1784
|
+
"travel": "Travel & places",
|
|
1785
|
+
"fashion": "Fashion & beauty",
|
|
1786
|
+
"home_garden": "Home & garden",
|
|
1787
|
+
"diy": "DIY & making",
|
|
1788
|
+
"automotive": "Cars & motoring",
|
|
1789
|
+
"animals": "Animals & pets",
|
|
1790
|
+
"family": "Family & parenting",
|
|
1791
|
+
"nonprofit": "Non-profit & charity",
|
|
1792
|
+
"government": "Government & public sector",
|
|
1793
|
+
"community": "Local community",
|
|
1794
|
+
"activism": "Activism & advocacy",
|
|
1795
|
+
"environment": "Environment & sustainability",
|
|
1796
|
+
"religion": "Religion & spirituality",
|
|
1797
|
+
"other": "Other"
|
|
1756
1798
|
},
|
|
1757
1799
|
"create": {
|
|
1758
1800
|
"title": "Create account",
|
|
@@ -1772,8 +1814,9 @@
|
|
|
1772
1814
|
"invalidChars": "Only letters, numbers, hyphens, and underscores",
|
|
1773
1815
|
"checkFailed": "Could not check availability"
|
|
1774
1816
|
},
|
|
1775
|
-
"
|
|
1776
|
-
"label": "
|
|
1817
|
+
"accountCategory": {
|
|
1818
|
+
"label": "Categories",
|
|
1819
|
+
"hint": "Pick up to {{max}}. The first one is your main category."
|
|
1777
1820
|
},
|
|
1778
1821
|
"toasts": {
|
|
1779
1822
|
"success": "Account created",
|
|
@@ -1748,11 +1748,53 @@
|
|
|
1748
1748
|
"description": "Una cuenta programática con credenciales de servicio"
|
|
1749
1749
|
}
|
|
1750
1750
|
},
|
|
1751
|
-
"
|
|
1751
|
+
"accountCategory": {
|
|
1752
|
+
"news": "Noticias",
|
|
1753
|
+
"politics": "Política",
|
|
1754
|
+
"business": "Negocios",
|
|
1755
|
+
"startup": "Startups y emprendimiento",
|
|
1756
|
+
"finance": "Finanzas e inversión",
|
|
1757
|
+
"crypto": "Cripto y web3",
|
|
1758
|
+
"marketplace": "Marketplace y anuncios",
|
|
1759
|
+
"retail": "Comercio y ecommerce",
|
|
1760
|
+
"real_estate": "Inmobiliaria",
|
|
1752
1761
|
"agency": "Agencia inmobiliaria",
|
|
1762
|
+
"landlord": "Casero y gestión de fincas",
|
|
1753
1763
|
"cooperative": "Cooperativa de vivienda",
|
|
1754
|
-
"
|
|
1755
|
-
"
|
|
1764
|
+
"architecture": "Arquitectura y construcción",
|
|
1765
|
+
"technology": "Tecnología",
|
|
1766
|
+
"software": "Software y desarrollo",
|
|
1767
|
+
"ai": "IA y aprendizaje automático",
|
|
1768
|
+
"security": "Seguridad y privacidad",
|
|
1769
|
+
"automation": "Automatización y herramientas",
|
|
1770
|
+
"science": "Ciencia e investigación",
|
|
1771
|
+
"education": "Educación y formación",
|
|
1772
|
+
"books": "Libros y escritura",
|
|
1773
|
+
"health": "Salud y medicina",
|
|
1774
|
+
"fitness": "Deporte y bienestar",
|
|
1775
|
+
"sports": "Deportes",
|
|
1776
|
+
"gaming": "Videojuegos y esports",
|
|
1777
|
+
"music": "Música",
|
|
1778
|
+
"film": "Cine y TV",
|
|
1779
|
+
"podcast": "Pódcast y audio",
|
|
1780
|
+
"art": "Arte y diseño",
|
|
1781
|
+
"photography": "Fotografía",
|
|
1782
|
+
"comedy": "Comedia",
|
|
1783
|
+
"food": "Comida y bebida",
|
|
1784
|
+
"travel": "Viajes y lugares",
|
|
1785
|
+
"fashion": "Moda y belleza",
|
|
1786
|
+
"home_garden": "Hogar y jardín",
|
|
1787
|
+
"diy": "Bricolaje y manualidades",
|
|
1788
|
+
"automotive": "Coches y motor",
|
|
1789
|
+
"animals": "Animales y mascotas",
|
|
1790
|
+
"family": "Familia y crianza",
|
|
1791
|
+
"nonprofit": "ONG y benéficas",
|
|
1792
|
+
"government": "Gobierno y sector público",
|
|
1793
|
+
"community": "Comunidad local",
|
|
1794
|
+
"activism": "Activismo y defensa",
|
|
1795
|
+
"environment": "Medio ambiente y sostenibilidad",
|
|
1796
|
+
"religion": "Religión y espiritualidad",
|
|
1797
|
+
"other": "Otra"
|
|
1756
1798
|
},
|
|
1757
1799
|
"create": {
|
|
1758
1800
|
"title": "Crear cuenta",
|
|
@@ -1772,8 +1814,9 @@
|
|
|
1772
1814
|
"invalidChars": "Solo letras, números, guiones y guiones bajos",
|
|
1773
1815
|
"checkFailed": "No se pudo comprobar la disponibilidad"
|
|
1774
1816
|
},
|
|
1775
|
-
"
|
|
1776
|
-
"label": "
|
|
1817
|
+
"accountCategory": {
|
|
1818
|
+
"label": "Categorías",
|
|
1819
|
+
"hint": "Elige hasta {{max}}. La primera es tu categoría principal."
|
|
1777
1820
|
},
|
|
1778
1821
|
"toasts": {
|
|
1779
1822
|
"success": "Cuenta creada",
|
|
@@ -1748,11 +1748,53 @@
|
|
|
1748
1748
|
"description": "A programmatic account with service credentials"
|
|
1749
1749
|
}
|
|
1750
1750
|
},
|
|
1751
|
-
"
|
|
1751
|
+
"accountCategory": {
|
|
1752
|
+
"news": "News",
|
|
1753
|
+
"politics": "Politics & policy",
|
|
1754
|
+
"business": "Business",
|
|
1755
|
+
"startup": "Startups & entrepreneurship",
|
|
1756
|
+
"finance": "Finance & investing",
|
|
1757
|
+
"crypto": "Crypto & web3",
|
|
1758
|
+
"marketplace": "Marketplace & classifieds",
|
|
1759
|
+
"retail": "Retail & ecommerce",
|
|
1760
|
+
"real_estate": "Real estate",
|
|
1752
1761
|
"agency": "Real estate agency",
|
|
1762
|
+
"landlord": "Landlord & property management",
|
|
1753
1763
|
"cooperative": "Housing cooperative",
|
|
1754
|
-
"
|
|
1755
|
-
"
|
|
1764
|
+
"architecture": "Architecture & construction",
|
|
1765
|
+
"technology": "Technology",
|
|
1766
|
+
"software": "Software & development",
|
|
1767
|
+
"ai": "AI & machine learning",
|
|
1768
|
+
"security": "Security & privacy",
|
|
1769
|
+
"automation": "Automation & tools",
|
|
1770
|
+
"science": "Science & research",
|
|
1771
|
+
"education": "Education & training",
|
|
1772
|
+
"books": "Books & writing",
|
|
1773
|
+
"health": "Health & medicine",
|
|
1774
|
+
"fitness": "Fitness & wellbeing",
|
|
1775
|
+
"sports": "Sports",
|
|
1776
|
+
"gaming": "Gaming & esports",
|
|
1777
|
+
"music": "Music",
|
|
1778
|
+
"film": "Film & TV",
|
|
1779
|
+
"podcast": "Podcasts & audio",
|
|
1780
|
+
"art": "Art & design",
|
|
1781
|
+
"photography": "Photography",
|
|
1782
|
+
"comedy": "Comedy",
|
|
1783
|
+
"food": "Food & drink",
|
|
1784
|
+
"travel": "Travel & places",
|
|
1785
|
+
"fashion": "Fashion & beauty",
|
|
1786
|
+
"home_garden": "Home & garden",
|
|
1787
|
+
"diy": "DIY & making",
|
|
1788
|
+
"automotive": "Cars & motoring",
|
|
1789
|
+
"animals": "Animals & pets",
|
|
1790
|
+
"family": "Family & parenting",
|
|
1791
|
+
"nonprofit": "Non-profit & charity",
|
|
1792
|
+
"government": "Government & public sector",
|
|
1793
|
+
"community": "Local community",
|
|
1794
|
+
"activism": "Activism & advocacy",
|
|
1795
|
+
"environment": "Environment & sustainability",
|
|
1796
|
+
"religion": "Religion & spirituality",
|
|
1797
|
+
"other": "Other"
|
|
1756
1798
|
},
|
|
1757
1799
|
"create": {
|
|
1758
1800
|
"title": "Create account",
|
|
@@ -1772,8 +1814,9 @@
|
|
|
1772
1814
|
"invalidChars": "Only letters, numbers, hyphens, and underscores",
|
|
1773
1815
|
"checkFailed": "Could not check availability"
|
|
1774
1816
|
},
|
|
1775
|
-
"
|
|
1776
|
-
"label": "
|
|
1817
|
+
"accountCategory": {
|
|
1818
|
+
"label": "Categories",
|
|
1819
|
+
"hint": "Pick up to {{max}}. The first one is your main category."
|
|
1777
1820
|
},
|
|
1778
1821
|
"toasts": {
|
|
1779
1822
|
"success": "Account created",
|
|
@@ -1748,11 +1748,53 @@
|
|
|
1748
1748
|
"description": "Una cuenta programática con credenciales de servicio"
|
|
1749
1749
|
}
|
|
1750
1750
|
},
|
|
1751
|
-
"
|
|
1751
|
+
"accountCategory": {
|
|
1752
|
+
"news": "Noticias",
|
|
1753
|
+
"politics": "Política",
|
|
1754
|
+
"business": "Negocios",
|
|
1755
|
+
"startup": "Startups y emprendimiento",
|
|
1756
|
+
"finance": "Finanzas e inversión",
|
|
1757
|
+
"crypto": "Cripto y web3",
|
|
1758
|
+
"marketplace": "Marketplace y anuncios",
|
|
1759
|
+
"retail": "Comercio y ecommerce",
|
|
1760
|
+
"real_estate": "Inmobiliaria",
|
|
1752
1761
|
"agency": "Agencia inmobiliaria",
|
|
1762
|
+
"landlord": "Casero y gestión de fincas",
|
|
1753
1763
|
"cooperative": "Cooperativa de vivienda",
|
|
1754
|
-
"
|
|
1755
|
-
"
|
|
1764
|
+
"architecture": "Arquitectura y construcción",
|
|
1765
|
+
"technology": "Tecnología",
|
|
1766
|
+
"software": "Software y desarrollo",
|
|
1767
|
+
"ai": "IA y aprendizaje automático",
|
|
1768
|
+
"security": "Seguridad y privacidad",
|
|
1769
|
+
"automation": "Automatización y herramientas",
|
|
1770
|
+
"science": "Ciencia e investigación",
|
|
1771
|
+
"education": "Educación y formación",
|
|
1772
|
+
"books": "Libros y escritura",
|
|
1773
|
+
"health": "Salud y medicina",
|
|
1774
|
+
"fitness": "Deporte y bienestar",
|
|
1775
|
+
"sports": "Deportes",
|
|
1776
|
+
"gaming": "Videojuegos y esports",
|
|
1777
|
+
"music": "Música",
|
|
1778
|
+
"film": "Cine y TV",
|
|
1779
|
+
"podcast": "Pódcast y audio",
|
|
1780
|
+
"art": "Arte y diseño",
|
|
1781
|
+
"photography": "Fotografía",
|
|
1782
|
+
"comedy": "Comedia",
|
|
1783
|
+
"food": "Comida y bebida",
|
|
1784
|
+
"travel": "Viajes y lugares",
|
|
1785
|
+
"fashion": "Moda y belleza",
|
|
1786
|
+
"home_garden": "Hogar y jardín",
|
|
1787
|
+
"diy": "Bricolaje y manualidades",
|
|
1788
|
+
"automotive": "Coches y motor",
|
|
1789
|
+
"animals": "Animales y mascotas",
|
|
1790
|
+
"family": "Familia y crianza",
|
|
1791
|
+
"nonprofit": "ONG y benéficas",
|
|
1792
|
+
"government": "Gobierno y sector público",
|
|
1793
|
+
"community": "Comunidad local",
|
|
1794
|
+
"activism": "Activismo y defensa",
|
|
1795
|
+
"environment": "Medio ambiente y sostenibilidad",
|
|
1796
|
+
"religion": "Religión y espiritualidad",
|
|
1797
|
+
"other": "Otra"
|
|
1756
1798
|
},
|
|
1757
1799
|
"create": {
|
|
1758
1800
|
"title": "Crear cuenta",
|
|
@@ -1772,8 +1814,9 @@
|
|
|
1772
1814
|
"invalidChars": "Solo letras, números, guiones y guiones bajos",
|
|
1773
1815
|
"checkFailed": "No se pudo comprobar la disponibilidad"
|
|
1774
1816
|
},
|
|
1775
|
-
"
|
|
1776
|
-
"label": "
|
|
1817
|
+
"accountCategory": {
|
|
1818
|
+
"label": "Categorías",
|
|
1819
|
+
"hint": "Elige hasta {{max}}. La primera es tu categoría principal."
|
|
1777
1820
|
},
|
|
1778
1821
|
"toasts": {
|
|
1779
1822
|
"success": "Cuenta creada",
|
package/dist/esm/index.js
CHANGED
|
@@ -38,7 +38,7 @@ export { OxyAppDataIdentifierError } from './mixins/OxyServices.appData.js';
|
|
|
38
38
|
export { getNormalizedUserId, normalizeUserIdentity, normalizeUserIdentityOrNull, } from './utils/userIdentity.js';
|
|
39
39
|
export { getCanonicalUserHandle, getNormalizedUserHandle, } from './utils/userHandle.js';
|
|
40
40
|
export { normalizeProfileLinks } from './utils/profileLinks.js';
|
|
41
|
-
export {
|
|
41
|
+
export { ACCOUNT_CATEGORY_IDS, MAX_ACCOUNT_CATEGORIES, SELECTABLE_ACCOUNT_CATEGORY_IDS, isSelectableAccountCategoryId, kindAcceptsAccountCategories, } from './mixins/OxyServices.accounts.js';
|
|
42
42
|
// ---------------------------------------------------------------------------
|
|
43
43
|
// Reputation (Oxy Trust: ledger, balances, disputes, rules, influence).
|
|
44
44
|
// The whole type family — the closed value sets, the two balance views and the
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { normalizeUserIdentity } from '../utils/userIdentity.js';
|
|
2
|
+
import { evictOxyIdentityCache } from '../utils/identityCacheSweep.js';
|
|
2
3
|
import { CACHE_TIMES } from './mixinHelpers.js';
|
|
3
|
-
export { ACCOUNT_KINDS,
|
|
4
|
+
export { ACCOUNT_CATEGORY_IDS, ACCOUNT_KINDS, MAX_ACCOUNT_CATEGORIES, SELECTABLE_ACCOUNT_CATEGORY_IDS, isActAsEligibleKind, isSelectableAccountCategoryId, kindAcceptsAccountCategories, } from '@oxyhq/contracts';
|
|
4
5
|
export function OxyServicesAccountsMixin(Base) {
|
|
5
6
|
return class extends Base {
|
|
6
7
|
constructor(...args) {
|
|
@@ -158,6 +159,18 @@ export function OxyServicesAccountsMixin(Base) {
|
|
|
158
159
|
/**
|
|
159
160
|
* Update an account's mutable profile fields. Tree placement changes
|
|
160
161
|
* (reparenting) go through the dedicated move endpoint, not here.
|
|
162
|
+
*
|
|
163
|
+
* An account IS a user, so this write changes identity — and a profile
|
|
164
|
+
* screen never reads `/accounts/<id>`. It reads `GET /users/<id>` and
|
|
165
|
+
* `GET /profiles/username/<handle>`, both cached for 5 minutes in the
|
|
166
|
+
* CALLER'S OWN process, so busting only the account-graph keys left every
|
|
167
|
+
* profile surface serving the pre-edit avatar and name for the full TTL
|
|
168
|
+
* with a perfectly healthy server (the cross-service `oxy:user:invalidate`
|
|
169
|
+
* signal does not help: it evicts BACKEND caches, and cannot reach a cache
|
|
170
|
+
* living in a browser tab). {@link evictOxyIdentityCache} owns that key
|
|
171
|
+
* list — see its docs for why the handle-keyed entries are prefix-swept
|
|
172
|
+
* (a RENAME leaves the old handle's entry unreachable by any targeted key).
|
|
173
|
+
*
|
|
161
174
|
* @param accountId - The account's Mongo `_id`.
|
|
162
175
|
* @param data - Subset of updatable profile fields.
|
|
163
176
|
*/
|
|
@@ -168,6 +181,14 @@ export function OxyServicesAccountsMixin(Base) {
|
|
|
168
181
|
// data) so neither serves the pre-update snapshot.
|
|
169
182
|
this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(accountId)}`);
|
|
170
183
|
this._invalidateAccountLists();
|
|
184
|
+
// The parent's children list embeds this account's profile and is keyed
|
|
185
|
+
// by the PARENT id, so it is reachable only from the response node.
|
|
186
|
+
const parentAccountId = res.account?.parentAccountId;
|
|
187
|
+
if (parentAccountId) {
|
|
188
|
+
this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(parentAccountId)}/children`);
|
|
189
|
+
}
|
|
190
|
+
// Every identity read of this account, whichever key it lands under.
|
|
191
|
+
evictOxyIdentityCache(this, accountId);
|
|
171
192
|
return res.account;
|
|
172
193
|
}
|
|
173
194
|
catch (error) {
|
|
@@ -3,6 +3,7 @@ import { buildQueryParams, buildPaginationParams, } from '../utils/apiUtils.js';
|
|
|
3
3
|
import { KeyManager } from '../crypto/keyManager.js';
|
|
4
4
|
import { SignatureService } from '../crypto/signatureService.js';
|
|
5
5
|
import { normalizeUserIdentity, normalizeUserIdentityOrNull } from '../utils/userIdentity.js';
|
|
6
|
+
import { evictOxyIdentityCache } from '../utils/identityCacheSweep.js';
|
|
6
7
|
import { logger } from '../logger/index.js';
|
|
7
8
|
import { extractErrorStatus } from '../utils/errorUtils.js';
|
|
8
9
|
/**
|
|
@@ -344,28 +345,22 @@ export function OxyServicesUserMixin(Base) {
|
|
|
344
345
|
/**
|
|
345
346
|
* Update user profile.
|
|
346
347
|
*
|
|
347
|
-
* Invalidates the SDK-side response cache for every endpoint that
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
348
|
+
* Invalidates the SDK-side response cache for every endpoint that can
|
|
349
|
+
* return this user — the list is owned by {@link evictOxyIdentityCache}, so
|
|
350
|
+
* a new identity read is added in one place instead of to each writer
|
|
351
|
+
* separately (this method's own hand-written copy had already drifted from
|
|
352
|
+
* the server-side one, missing `GET /auth/lookup/*` and
|
|
353
|
+
* `GET /profiles/resolve`). Without the sweep a follow-up
|
|
354
|
+
* `getUserBySession` inside the cache window returns the pre-update user —
|
|
355
|
+
* most visibly during onboarding, where the username step flickers back as
|
|
356
|
+
* if nothing was saved.
|
|
354
357
|
*
|
|
355
358
|
* TanStack Query handles offline queuing automatically.
|
|
356
359
|
*/
|
|
357
360
|
async updateProfile(updates) {
|
|
358
361
|
try {
|
|
359
362
|
const result = normalizeUserIdentity(await this.makeRequest('PUT', '/users/me', updates, { cache: false }));
|
|
360
|
-
|
|
361
|
-
// prefix sweep rather than an enumeration because the SDK never
|
|
362
|
-
// tracks the set of active session IDs centrally.
|
|
363
|
-
this.clearCacheByPrefix('GET:/session/user/');
|
|
364
|
-
this.clearCacheByPrefix('GET:/users/me');
|
|
365
|
-
this.clearCacheByPrefix('GET:/profiles/username/');
|
|
366
|
-
if (result?.id) {
|
|
367
|
-
this.clearCacheEntry(`GET:/users/${result.id}`);
|
|
368
|
-
}
|
|
363
|
+
evictOxyIdentityCache(this, result?.id);
|
|
369
364
|
return result;
|
|
370
365
|
}
|
|
371
366
|
catch (error) {
|
|
@@ -418,10 +413,9 @@ export function OxyServicesUserMixin(Base) {
|
|
|
418
413
|
const result = await this.makeRequest('PATCH', `/privacy/${id}/privacy`, settings, {
|
|
419
414
|
cache: false,
|
|
420
415
|
});
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
this
|
|
424
|
-
this.clearCacheEntry(`GET:/users/${id}`);
|
|
416
|
+
// Privacy settings ride the user DTO, so every identity read goes stale
|
|
417
|
+
// too — same key list as any other profile write.
|
|
418
|
+
evictOxyIdentityCache(this, id);
|
|
425
419
|
this.clearCacheEntry(`GET:/privacy/${id}/privacy`);
|
|
426
420
|
return result;
|
|
427
421
|
}
|
package/dist/esm/server/index.js
CHANGED
|
@@ -27,7 +27,11 @@ export { buildOxyCspDirectives, buildOxyPagesHeaders, createOxySecurityHeaders,
|
|
|
27
27
|
export { verifySecret } from './verifySecret.js';
|
|
28
28
|
// Cross-service user-invalidation signal: oxy-api publishes when identity
|
|
29
29
|
// changes, every consuming backend sweeps its caches instead of waiting out a TTL.
|
|
30
|
-
export { createOxyUserInvalidationHandler,
|
|
30
|
+
export { createOxyUserInvalidationHandler, publishOxyUserInvalidation, } from './userInvalidation.js';
|
|
31
|
+
// The identity-key enumeration itself is platform-neutral (`src/utils/`) so the
|
|
32
|
+
// client mixins and this Node-only subscriber sweep the SAME list — a second
|
|
33
|
+
// copy is what let `updateAccount` and `updateProfile` drift apart.
|
|
34
|
+
export { evictOxyIdentityCache, oxyUserByIdCacheKey, OXY_IDENTITY_CACHE_PREFIXES } from '../utils/identityCacheSweep.js';
|
|
31
35
|
// Registrable-apex (eTLD+1) derivation via the Public Suffix List — the SINGLE
|
|
32
36
|
// SOURCE OF TRUTH shared with the IdP worker and the client FAPI auto-detect.
|
|
33
37
|
// Pure host handling (no browser deps), so it is safe on the server subpath and
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
* Every Oxy backend caches Oxy identity, and none of them find out when it
|
|
7
7
|
* changes. The `OxyServices` GET response cache holds `GET /users/:id` and
|
|
8
8
|
* `GET /profiles/username/:name` for five minutes; it is swept when THIS process
|
|
9
|
-
* writes the profile (
|
|
10
|
-
* never when somebody else does — which is the normal case, since
|
|
11
|
-
* edited in Oxy's own apps. So an avatar or display-name change is
|
|
12
|
-
* every consuming backend for up to five minutes, per process.
|
|
9
|
+
* writes the profile (the `evictOxyIdentityCache` calls in the user and accounts
|
|
10
|
+
* mixins) and never when somebody else does — which is the normal case, since
|
|
11
|
+
* profiles are edited in Oxy's own apps. So an avatar or display-name change is
|
|
12
|
+
* invisible to every consuming backend for up to five minutes, per process.
|
|
13
13
|
*
|
|
14
14
|
* oxy-api broadcasts {@link OXY_USER_INVALIDATION_CHANNEL} on the shared Valkey
|
|
15
15
|
* when a user's identity changes. This module is the consumer half: it parses
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
* Node-only; exported solely from `@oxyhq/core/server`.
|
|
52
52
|
*/
|
|
53
53
|
import { OXY_USER_INVALIDATION_CHANNEL, isPublishedOxyUserChangeReason, oxyUserInvalidationEventSchema, } from '@oxyhq/contracts';
|
|
54
|
+
import { evictOxyIdentityCache, } from '../utils/identityCacheSweep.js';
|
|
54
55
|
/**
|
|
55
56
|
* Broadcast that an Oxy user's record changed.
|
|
56
57
|
*
|
|
@@ -149,25 +150,3 @@ export function createOxyUserInvalidationHandler(options = {}) {
|
|
|
149
150
|
}
|
|
150
151
|
};
|
|
151
152
|
}
|
|
152
|
-
/**
|
|
153
|
-
* Sweep an `OxyServices` GET response cache of everything that could carry the
|
|
154
|
-
* given user's identity.
|
|
155
|
-
*
|
|
156
|
-
* The by-id entry is exact. The by-username and resolve entries are keyed by
|
|
157
|
-
* HANDLE, which cannot be derived from an id without the very lookup we are
|
|
158
|
-
* invalidating, so those are swept by prefix — the same imprecision the SDK
|
|
159
|
-
* already accepts when it sweeps its own cache after a local profile write, and
|
|
160
|
-
* bounded by the fact that over-eviction costs a refetch and can never serve
|
|
161
|
-
* wrong data.
|
|
162
|
-
*/
|
|
163
|
-
export function evictOxyIdentityCache(oxy, userId) {
|
|
164
|
-
// Match the sweep the user mixin runs after a local profile write — session-
|
|
165
|
-
// bound and /users/me entries are keyed without the user id, so they must be
|
|
166
|
-
// prefix-swept on cross-service invalidation too.
|
|
167
|
-
oxy.clearCacheByPrefix('GET:/session/user/');
|
|
168
|
-
oxy.clearCacheByPrefix('GET:/users/me');
|
|
169
|
-
oxy.clearCacheByPrefix('GET:/auth/lookup/');
|
|
170
|
-
oxy.clearCacheEntry(`GET:/users/${userId}`);
|
|
171
|
-
oxy.clearCacheByPrefix('GET:/profiles/username/');
|
|
172
|
-
oxy.clearCacheByPrefix('GET:/profiles/resolve');
|
|
173
|
-
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE enumeration of `OxyServices` GET-cache keys that can carry a single
|
|
3
|
+
* account's identity, and the one sweep that clears them.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS IS ONE LIST
|
|
6
|
+
* --------------------
|
|
7
|
+
* An Oxy account is readable under SEVERAL cache keys, and a write that only
|
|
8
|
+
* busts the key it happens to know about leaves every other one serving the
|
|
9
|
+
* pre-write snapshot for up to its TTL — from the caller's OWN in-memory cache,
|
|
10
|
+
* with a perfectly healthy server. That failure has already shipped twice with
|
|
11
|
+
* two different sets of keys:
|
|
12
|
+
*
|
|
13
|
+
* - `updateAccount` busted `GET:/accounts/<id>` and the account lists, but a
|
|
14
|
+
* profile screen reads `GET:/profiles/username/<name>` and
|
|
15
|
+
* `GET:/users/<id>`, so a channel's new picture stayed invisible for the
|
|
16
|
+
* full 5-minute profile TTL.
|
|
17
|
+
* - `updateProfile` busted four of the six keys below, missing
|
|
18
|
+
* `GET:/auth/lookup/` (the login-flow avatar/display-name lookup) and
|
|
19
|
+
* `GET:/profiles/resolve` (handle resolution) — two independently-drifted
|
|
20
|
+
* copies of a list that has to agree.
|
|
21
|
+
*
|
|
22
|
+
* So the list lives here, once, and every writer calls
|
|
23
|
+
* {@link evictOxyIdentityCache}. Adding a new identity read means adding its key
|
|
24
|
+
* HERE and every writer inherits it.
|
|
25
|
+
*
|
|
26
|
+
* WHERE THE LINE IS DRAWN
|
|
27
|
+
* -----------------------
|
|
28
|
+
* These are the SINGLE-PROFILE reads — the account is the subject of the
|
|
29
|
+
* response and is addressable by id, handle, or session. Reads that merely
|
|
30
|
+
* CONTAIN an account among many (`GET:/profiles/search`,
|
|
31
|
+
* `GET:/users/<other>/followers`, `GET:/profiles/<other>/similar`) are
|
|
32
|
+
* deliberately NOT swept: an account cannot be located in them without the very
|
|
33
|
+
* lookup being invalidated, so sweeping them means sweeping the whole namespace
|
|
34
|
+
* on every identity change — a real cost on a backend consuming the
|
|
35
|
+
* cross-service invalidation signal, for a surface where a stale thumbnail
|
|
36
|
+
* expires on its own in ~2 minutes.
|
|
37
|
+
*
|
|
38
|
+
* WHY PREFIXES RATHER THAN EXACT KEYS
|
|
39
|
+
* -----------------------------------
|
|
40
|
+
* Only the by-id key can be built from a user id. The handle-keyed and
|
|
41
|
+
* session-keyed entries cannot — deriving a handle from an id needs the lookup
|
|
42
|
+
* we are invalidating, and the SDK never tracks active session ids centrally.
|
|
43
|
+
* Prefix sweeping is also what makes a USERNAME CHANGE correct: the entry under
|
|
44
|
+
* the OLD handle is unreachable by construction (nothing in the write response
|
|
45
|
+
* carries it), and a sweep targeted at the new handle alone would leave the old
|
|
46
|
+
* one serving the pre-rename profile until its TTL. Over-eviction costs a
|
|
47
|
+
* refetch; under-eviction serves wrong data.
|
|
48
|
+
*
|
|
49
|
+
* Platform-neutral by construction (no imports, no `OxyServices` reference) so
|
|
50
|
+
* the client mixins and the Node-only `@oxyhq/core/server` invalidation
|
|
51
|
+
* subscriber can share it without either pulling in the other.
|
|
52
|
+
*/
|
|
53
|
+
/**
|
|
54
|
+
* Cache-key PREFIXES under which an account's identity can be served, for the
|
|
55
|
+
* reads whose key cannot be derived from a user id. Swept wholesale.
|
|
56
|
+
*/
|
|
57
|
+
export const OXY_IDENTITY_CACHE_PREFIXES = [
|
|
58
|
+
// `getUserBySession` — keyed by session id, which the SDK never enumerates.
|
|
59
|
+
'GET:/session/user/',
|
|
60
|
+
// `getCurrentUser` (and `GET:/users/me/graph`, harmlessly included).
|
|
61
|
+
'GET:/users/me',
|
|
62
|
+
// `lookupUsername` — the pre-session login lookup; carries avatar + display name.
|
|
63
|
+
'GET:/auth/lookup/',
|
|
64
|
+
// `getProfileByUsername` — keyed by handle, including the pre-rename handle.
|
|
65
|
+
'GET:/profiles/username/',
|
|
66
|
+
// `resolveProfile` — keyed by fediverse handle in the query payload.
|
|
67
|
+
'GET:/profiles/resolve',
|
|
68
|
+
];
|
|
69
|
+
/**
|
|
70
|
+
* Build the exact cache key `getUserById` reads under. The only identity key
|
|
71
|
+
* derivable from a user id, so the only one that does not need a prefix sweep.
|
|
72
|
+
*/
|
|
73
|
+
export function oxyUserByIdCacheKey(userId) {
|
|
74
|
+
return `GET:/users/${userId}`;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Sweep an `OxyServices` GET response cache of everything that could carry the
|
|
78
|
+
* given account's identity.
|
|
79
|
+
*
|
|
80
|
+
* @param oxy - Anything exposing the SDK's two eviction methods.
|
|
81
|
+
* @param userId - The account whose by-id entry to drop. Optional: a caller
|
|
82
|
+
* that does not know the id still clears every handle-, session-
|
|
83
|
+
* and self-keyed entry, which is the majority of the surface.
|
|
84
|
+
*/
|
|
85
|
+
export function evictOxyIdentityCache(oxy, userId) {
|
|
86
|
+
for (const prefix of OXY_IDENTITY_CACHE_PREFIXES) {
|
|
87
|
+
oxy.clearCacheByPrefix(prefix);
|
|
88
|
+
}
|
|
89
|
+
if (userId) {
|
|
90
|
+
oxy.clearCacheEntry(oxyUserByIdCacheKey(userId));
|
|
91
|
+
}
|
|
92
|
+
}
|