This commit is contained in:
seb
2026-09-22 07:59:00 +02:00
parent 5e13b8f9cd
commit 51d01a502b
5 changed files with 220 additions and 80 deletions

View File

@@ -79,7 +79,7 @@ function createCache(label, load) {
export class StorageManager extends ApiClient {
/** @type {EventSource | null} */
#events = null;
/** @type {ReturnType<typeof createCache> | null} */
/** @type {ReturnType<StorageManager['#createDeferredCache']> | null} */
#articleIndexCache = null;
/** @type {Map<string, Set<(article: object) => void>>} sku → article listeners */
#articleWatchers = new Map();
@@ -386,14 +386,24 @@ export class StorageManager extends ApiClient {
#initCaches() {
const authMeCache = createCache('GET /auth/me', () => this.#loadAuthMe());
const deliveryCache = createCache('GET /delivery', () => this.#loadDeliveries());
const customergroupCache = createCache('GET /customergroup', () =>
const deliveryCache = this.#createDeferredCache('GET /delivery', () =>
this.#loadDeliveries(),
);
const customergroupCache = this.#createDeferredCache('GET /customergroup', () =>
this.#loadCustomergroups(),
);
const entityCache = createCache('GET /entity', () => this.#loadEntities());
const numberCache = createCache('GET /number', () => this.#loadNumbers());
const quoteCache = createCache('GET /quote', () => this.#loadQuotes());
const orderCache = createCache('GET /order', () => this.#loadOrders());
const entityCache = this.#createDeferredCache('GET /entity', () =>
this.#loadEntities(),
);
const numberCache = this.#createDeferredCache('GET /number', () =>
this.#loadNumbers(),
);
const quoteCache = this.#createDeferredCache('GET /quote', () =>
this.#loadQuotes(),
);
const orderCache = this.#createDeferredCache('GET /order', () =>
this.#loadOrders(),
);
// articleIndex is auth-gated — create a deferred cache that only loads after auth
this.#articleIndexCache = this.#createDeferredCache(
@@ -478,7 +488,7 @@ export class StorageManager extends ApiClient {
/** Settled GET /category/tree body, or undefined. */
categoryTree: { get: () => categoryTreeCache.value },
// ── Remaining non-auth-gated caches ─────────────────────────────────────
// ── Deferred caches (auth-gated) ─────────────────────────────────────────
deliveryPromise: { get: () => deliveryCache.promise },
updateDelivery: { value: deliveryCache.update },
/** Settled GET /delivery list, or undefined. */
@@ -513,58 +523,132 @@ export class StorageManager extends ApiClient {
reconnectEvents: { value: () => this.#reconnectEvents() },
});
// Trigger articleIndex load once auth is established
authAutomat.subscribe((auth) => {
if (auth.status === 'authed' && auth.user?.selectedCompanyId) {
if (!articleIndexCache.value) {
void articleIndexCache.update?.().catch(() => {});
let lastCompanyId = null;
const syncWithAuth = (auth) => {
const companyId =
auth.status === 'authed' && auth.user?.selectedCompanyId != null
? String(auth.user.selectedCompanyId)
: null;
if (companyId === lastCompanyId) return;
const prevCompanyId = lastCompanyId;
lastCompanyId = companyId;
if (!companyId) {
// Unauthenticated or logged out
this.#disconnectEvents();
articleIndexCache.clear();
categoryTreeCache.clear();
deliveryCache.clear();
customergroupCache.clear();
entityCache.clear();
numberCache.clear();
quoteCache.clear();
orderCache.clear();
artikelAutomat.actions.clear();
} else {
// Company established or switched
if (prevCompanyId) {
articleIndexCache.clear();
categoryTreeCache.clear();
deliveryCache.clear();
customergroupCache.clear();
entityCache.clear();
numberCache.clear();
quoteCache.clear();
orderCache.clear();
artikelAutomat.actions.clear();
}
this.#reconnectEvents();
void articleIndexCache.trigger().catch(() => {});
void deliveryCache.trigger().catch(() => {});
void customergroupCache.trigger().catch(() => {});
void entityCache.trigger().catch(() => {});
void numberCache.trigger().catch(() => {});
void quoteCache.trigger().catch(() => {});
void orderCache.trigger().catch(() => {});
}
});
};
authAutomat.subscribe(syncWithAuth);
syncWithAuth(authAutomat.state);
}
/**
* Create a cache that does NOT load immediately but waits for an explicit trigger.
* Differs from createCache in that the initial load promise is not started.
* Create a cache that does NOT load immediately but waits for an explicit trigger
* or loads on-demand once authenticated.
*/
#createDeferredCache(label, load) {
let current;
let resolveInitial;
let rejectInitial;
let promise = new Promise((res, rej) => {
resolveInitial = res;
rejectInitial = rej;
});
let loadPromise = null;
async function update() {
const trigger = () => {
if (loadPromise) return loadPromise;
loadPromise = (async () => {
try {
const data = await load();
current = data;
return data;
} catch (err) {
console.error(`[storageMgr] ${label} failed`, err);
loadPromise = null;
throw err;
}
})();
return loadPromise;
};
const update = async () => {
try {
const data = await load();
current = data;
promise = Promise.resolve(data);
resolveInitial?.(data);
resolveInitial = null;
loadPromise = Promise.resolve(data);
return data;
} catch (err) {
console.error(`[storageMgr] update ${label} failed`, err);
rejectInitial?.(err);
rejectInitial = null;
throw err;
}
}
};
function replace(data) {
const replace = (data) => {
current = data;
promise = Promise.resolve(data);
resolveInitial?.(data);
resolveInitial = null;
loadPromise = Promise.resolve(data);
return data;
}
};
const clear = () => {
current = undefined;
loadPromise = null;
};
return {
update,
replace,
get promise() { return promise; },
get value() { return current; },
clear,
trigger,
get promise() {
if (!loadPromise) {
if (authAutomat.state.status === 'authed' && authAutomat.state.user?.selectedCompanyId) {
return trigger();
}
return new Promise((resolve, reject) => {
const unsub = authAutomat.subscribe((auth) => {
if (auth.status === 'authed' && auth.user?.selectedCompanyId) {
unsub();
trigger().then(resolve, reject);
} else if (auth.status === 'anon') {
unsub();
reject(new Error(`Cannot load ${label}: unauthenticated`));
}
});
});
}
return loadPromise;
},
get value() {
return current;
},
};
}
@@ -652,24 +736,6 @@ export class StorageManager extends ApiClient {
}));
}
async #loadBrands() {
const data = await this.brand.list();
requireObject(data, 'GET /api/brand');
return requireArray(data.brands, 'brands');
}
async #loadUnits() {
const data = await this.unit.list();
requireObject(data, 'GET /api/unit');
return requireArray(data.units, 'units');
}
async #loadCompanies() {
const data = await this.company.list();
requireObject(data, 'GET /api/company');
return requireArray(data.companies, 'companies');
}
async #loadCategoryTree() {
const data = await this.category.tree();
requireObject(data, 'GET /api/category/tree');
@@ -679,12 +745,6 @@ export class StorageManager extends ApiClient {
return data;
}
async #loadPosterminals() {
const data = await this.posterminal.list();
requireObject(data, 'GET /api/posterminal');
return requireArray(data.posterminals, 'posterminals');
}
async #loadDeliveries() {
const data = await this.delivery.list();
requireObject(data, 'GET /api/delivery');