442 lines
19 KiB
JavaScript
442 lines
19 KiB
JavaScript
|
|
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||
|
|
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||
|
|
|
||
|
|
// La génération (§ 5.7, § 5.8, § 5.10) : les exécuteurs qui font tourner la
|
||
|
|
// recherche du moteur, et le contrôleur qui la lance depuis la séance.
|
||
|
|
//
|
||
|
|
// Un exécuteur rend, pour une configuration et des réglages, une exécution :
|
||
|
|
// son résultat, une promesse des propositions de rechercher ; ses
|
||
|
|
// avancements ; son annulation, qui rejette le résultat par ErreurAnnulee,
|
||
|
|
// sans résultat partiel. L'exécuteur direct calcule dans le fil, une
|
||
|
|
// microtâche après le lancement : il sert les épreuves et le premier
|
||
|
|
// montage de l'application ; celui du travailleur (I13) calcule hors du fil
|
||
|
|
// et s'annule par terminate(). Une erreur de la recherche devient la même
|
||
|
|
// dans les deux, par versErreurGeneration.
|
||
|
|
//
|
||
|
|
// Le contrôleur ne numérote rien et ne mesure aucun plan. Il refuse avant
|
||
|
|
// tout lancement un réglage hors de son domaine (REGLAGE_HORS_DOMAINE), puis
|
||
|
|
// ce que l'enregistrement refuserait — les refus de la séance, lus dans son
|
||
|
|
// état observable, dans l'ordre de son contrat —, puis
|
||
|
|
// ce qui rend la génération vaine ou impossible ; il garde la graine
|
||
|
|
// proposée et la durée du dernier calcul, en mémoire, pour la séance de
|
||
|
|
// l'application ; il vérifie les invariants de chaque proposition rendue,
|
||
|
|
// écarte celles dont le plan est déjà présent, et confie les autres à la
|
||
|
|
// commande enregistrerGeneration, une seule entrée du journal. Le nombre de
|
||
|
|
// propositions d'une génération est un réglage, jamais une constante d'ici.
|
||
|
|
//
|
||
|
|
// Le compte d'inactivité de la séance est suspendu pendant toute la
|
||
|
|
// génération (debuterCalcul), et repart à sa fin, quelle qu'elle soit
|
||
|
|
// (finirCalcul, dans un finally). Passer en lecture, ou fermer l'événement,
|
||
|
|
// annule la génération en cours (surAnnulation).
|
||
|
|
import { normaliser, nombrePlacesManquantes } from '../moteur/configuration.js';
|
||
|
|
import { ErreurAnnulee, ErreurConfiguration } from '../moteur/erreurs.js';
|
||
|
|
import { rechercher, verifierReglages } from '../moteur/recherche.js';
|
||
|
|
import { verifierInvariants } from '../moteur/verification.js';
|
||
|
|
import { GENERATION_PAR_DEFAUT, configurationDepuisCharge } from '../stockage/document.js';
|
||
|
|
import { derive, planDepuisFichier, versFichier } from '../stockage/placements.js';
|
||
|
|
import { lireEntier } from './capacite.js';
|
||
|
|
|
||
|
|
// src/interface reçoit l'annulation d'ici : elle ne joint pas le moteur.
|
||
|
|
export { ErreurAnnulee } from '../moteur/erreurs.js';
|
||
|
|
|
||
|
|
/** Au-delà de cette durée estimée, en secondes, la génération demande confirmation (§ 5.10). */
|
||
|
|
export const SEUIL_GENERATION_LONGUE_S = 60;
|
||
|
|
|
||
|
|
/** Le plafond d'usage d'une génération, en secondes (§ 5.10) : un repère dit, jamais un refus. */
|
||
|
|
export const PLAFOND_USAGE_S = 30 * 60;
|
||
|
|
|
||
|
|
// Sans durée mesurée, une génération de plus de mouvements que ce compte —
|
||
|
|
// dix fois le réglage par défaut — demande confirmation.
|
||
|
|
const MOUVEMENTS_SANS_MESURE = 10 * GENERATION_PAR_DEFAUT.nombre * GENERATION_PAR_DEFAUT.arret;
|
||
|
|
|
||
|
|
// La plus grande graine, et le modulo de la suivante.
|
||
|
|
const GRAINE_MAX = 2 ** 32 - 1;
|
||
|
|
const GRAINES = 2 ** 32;
|
||
|
|
|
||
|
|
// Les champs des réglages, dans l'ordre où leurs refus se rendent, et le
|
||
|
|
// domaine de chacun ; max null : sans borne.
|
||
|
|
const CHAMPS = [
|
||
|
|
['graine', 0, GRAINE_MAX],
|
||
|
|
['nombre', 1, null],
|
||
|
|
['arret', 1, null],
|
||
|
|
['historique', 1, null],
|
||
|
|
];
|
||
|
|
|
||
|
|
const versSection = (section) => ({ geste: 'ouvrirSection', section });
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Refus ou échec d'une génération : son code et ses détails — ceux de la
|
||
|
|
* séance, du moteur ou du contrôleur, tels quels. Le texte affiché est celui
|
||
|
|
* de son code, nu (§ 14.6) ; le message, code et détails en JSON, sert aux
|
||
|
|
* traces.
|
||
|
|
*/
|
||
|
|
export class ErreurGeneration extends Error {
|
||
|
|
/**
|
||
|
|
* @param {string} code
|
||
|
|
* @param {Object} [details]
|
||
|
|
*/
|
||
|
|
constructor(code, details = {}) {
|
||
|
|
super(`${code} ${JSON.stringify(details)}`);
|
||
|
|
this.code = code;
|
||
|
|
this.details = details;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
ErreurGeneration.prototype.name = 'ErreurGeneration';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Les réglages d'une génération, contrôlés par la règle de rechercher
|
||
|
|
* (verifierReglages) avant tout calcul. Lève, pour le premier réglage hors
|
||
|
|
* de son domaine, ErreurGeneration('REGLAGE_HORS_DOMAINE', { reglage,
|
||
|
|
* remede: null }), reglage étant son nom — graine, arret, historique ou
|
||
|
|
* nombre —, sans le message du moteur ; ne rend rien sinon. Toute autre
|
||
|
|
* erreur du contrôle est relevée telle quelle.
|
||
|
|
*
|
||
|
|
* @param {Object} reglages
|
||
|
|
*/
|
||
|
|
export function exigerReglages(reglages) {
|
||
|
|
try {
|
||
|
|
verifierReglages(reglages);
|
||
|
|
} catch (erreur) {
|
||
|
|
if (erreur instanceof RangeError && typeof erreur.reglage === 'string') {
|
||
|
|
throw new ErreurGeneration('REGLAGE_HORS_DOMAINE', { reglage: erreur.reglage, remede: null });
|
||
|
|
}
|
||
|
|
throw erreur;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* L'erreur d'une recherche, la même dans les deux exécuteurs : ErreurAnnulee
|
||
|
|
* telle quelle ; une ErreurConfiguration devient ErreurGeneration de son code
|
||
|
|
* et de ses détails ; toute autre, RangeError comprise, est rendue telle
|
||
|
|
* quelle, pour être relevée. Les réglages sont contrôlés avant le calcul
|
||
|
|
* (exigerReglages) : une RangeError qui survient pendant lui vient d'ailleurs,
|
||
|
|
* d'un abonné aux avancements par exemple, et ne se dit pas comme un réglage.
|
||
|
|
*
|
||
|
|
* @param {unknown} erreur
|
||
|
|
* @returns {unknown}
|
||
|
|
*/
|
||
|
|
export function versErreurGeneration(erreur) {
|
||
|
|
if (erreur instanceof ErreurAnnulee) return erreur;
|
||
|
|
if (erreur instanceof ErreurConfiguration) return new ErreurGeneration(erreur.code, erreur.details);
|
||
|
|
return erreur;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @typedef {Object} Execution
|
||
|
|
* @property {(fn: (fait: number, total: number) => void) => () => void} surAvancement
|
||
|
|
* abonne fn aux avancements ; rend la fonction qui désabonne
|
||
|
|
* @property {Promise<import('../moteur/recherche.js').Proposition[]>} resultat
|
||
|
|
* rejetée par ErreurAnnulee à l'annulation, par ErreurGeneration sur
|
||
|
|
* une erreur du moteur
|
||
|
|
* @property {() => void} annuler sans résultat partiel
|
||
|
|
*
|
||
|
|
* @typedef {{lancer: (configuration: Object, reglages: Object) => Execution}} Executeur
|
||
|
|
*/
|
||
|
|
|
||
|
|
/**
|
||
|
|
* L'exécuteur direct : rechercher dans le fil, une microtâche après lancer.
|
||
|
|
* Rien d'autre ne s'exécute pendant la recherche. annuler(), avant ce départ
|
||
|
|
* ou depuis un rappel d'avancement, fait lever ErreurAnnulee à la lecture
|
||
|
|
* suivante du signal par le moteur : avant la proposition suivante, au plus
|
||
|
|
* 1 000 mouvements plus tard. Chaque avancement est celui du moteur, toutes
|
||
|
|
* les 1 000 itérations.
|
||
|
|
*
|
||
|
|
* @returns {Executeur}
|
||
|
|
*/
|
||
|
|
export function creerExecuteurDirect() {
|
||
|
|
return {
|
||
|
|
lancer(configuration, reglages) {
|
||
|
|
const signal = { aborted: false };
|
||
|
|
const abonnes = new Set();
|
||
|
|
const progression = (fait, total) => {
|
||
|
|
for (const fn of [...abonnes]) fn(fait, total);
|
||
|
|
};
|
||
|
|
const resultat = Promise.resolve().then(() => {
|
||
|
|
exigerReglages(reglages);
|
||
|
|
try {
|
||
|
|
return rechercher(configuration, reglages, { signal, progression });
|
||
|
|
} catch (erreur) {
|
||
|
|
throw versErreurGeneration(erreur);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
return {
|
||
|
|
surAvancement(fn) {
|
||
|
|
abonnes.add(fn);
|
||
|
|
return () => {
|
||
|
|
abonnes.delete(fn);
|
||
|
|
};
|
||
|
|
},
|
||
|
|
resultat,
|
||
|
|
annuler() {
|
||
|
|
signal.aborted = true;
|
||
|
|
},
|
||
|
|
};
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// Le plan d'une proposition du moteur, rangé comme planDepuisFichier rend
|
||
|
|
// celui d'une proposition enregistrée : la même mise en forme, versFichier
|
||
|
|
// puis planDepuisFichier, pour que deux plans égaux s'écrivent pareil.
|
||
|
|
function planRange(proposition, configuration, produitVersion) {
|
||
|
|
const fichier = versFichier(proposition, configuration, { produitVersion, attribuerSieges: false, decalage: 0 });
|
||
|
|
return JSON.stringify(planDepuisFichier(fichier));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Le contrôleur de génération d'une séance (§ 5.7, § 5.10).
|
||
|
|
*
|
||
|
|
* @param {Object} parametres
|
||
|
|
* @param {Object} parametres.seance la séance d'A1 et A2 : etat,
|
||
|
|
* subscribe, surAnnulation, executer, debuterCalcul, finirCalcul
|
||
|
|
* @param {Executeur} parametres.executeur
|
||
|
|
* @param {import('./horloge.js').Horloge} parametres.horloge instant() mesure
|
||
|
|
* la durée d'un calcul
|
||
|
|
* @param {string} parametres.produitVersion
|
||
|
|
* @returns {Object} le contrôleur : grainePropose, reglagesProposes, valider,
|
||
|
|
* estimer, derniereGeneration, generer, annuler, subscribe
|
||
|
|
*/
|
||
|
|
export function creerControleurGeneration({ seance, executeur, horloge, produitVersion }) {
|
||
|
|
// graine : la graine proposée ; base : celle de l'événement ouvert, null
|
||
|
|
// sans événement ; mesure : le dernier calcul abouti, null avant lui ;
|
||
|
|
// execution : celle en cours, null hors génération ; enCours vaut vrai du
|
||
|
|
// lancement à la fin de l'enregistrement.
|
||
|
|
let graine = 1;
|
||
|
|
let base = null;
|
||
|
|
let mesure = null;
|
||
|
|
let execution = null;
|
||
|
|
let observable = { enCours: false, fait: 0, total: 0, annulee: null };
|
||
|
|
const abonnes = new Set();
|
||
|
|
|
||
|
|
function publier(changements) {
|
||
|
|
observable = { ...observable, ...changements };
|
||
|
|
for (const fn of [...abonnes]) fn(observable);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Une ouverture : l'événement passe de null, ou d'une autre base, à une
|
||
|
|
// base ; la graine proposée repart de 1.
|
||
|
|
seance.subscribe((etat) => {
|
||
|
|
const suivante = etat.evenement?.base ?? null;
|
||
|
|
if (suivante !== null && suivante !== base) graine = 1;
|
||
|
|
base = suivante;
|
||
|
|
});
|
||
|
|
|
||
|
|
// Les réglages de génération de la charge ouverte, ceux d'une charge
|
||
|
|
// neuve sans événement.
|
||
|
|
const reglagesDeLaCharge = () => seance.etat().charge?.reglages.generation ?? GENERATION_PAR_DEFAUT;
|
||
|
|
|
||
|
|
function annulerAvec(cause) {
|
||
|
|
if (execution === null) return;
|
||
|
|
const courante = execution;
|
||
|
|
publier({ annulee: cause });
|
||
|
|
courante.annuler();
|
||
|
|
}
|
||
|
|
|
||
|
|
function estimer({ nombre, arret }) {
|
||
|
|
const mouvements = nombre * arret;
|
||
|
|
if (mesure === null) return { inconnue: true };
|
||
|
|
const secondes = (mouvements * mesure.dureeMs) / (mesure.mouvements * 1000);
|
||
|
|
return {
|
||
|
|
secondes,
|
||
|
|
longue: secondes > SEUIL_GENERATION_LONGUE_S,
|
||
|
|
auDelaDuPlafond: secondes > PLAFOND_USAGE_S,
|
||
|
|
mesure: { ...mesure },
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// Longue : au-delà du seuil estimé, ou, sans mesure, au-delà de dix fois
|
||
|
|
// le réglage par défaut.
|
||
|
|
function estLongue(reglages, estimation) {
|
||
|
|
if (estimation.inconnue === true) return reglages.nombre * reglages.arret > MOUVEMENTS_SANS_MESURE;
|
||
|
|
return estimation.longue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Les refus d'avant lancement : les réglages d'abord, puis l'ordre du
|
||
|
|
// contrat ; rend la configuration et son instance quand la génération peut
|
||
|
|
// partir.
|
||
|
|
function exigerLancement(reglages, confirmee) {
|
||
|
|
exigerReglages(reglages);
|
||
|
|
const etat = seance.etat();
|
||
|
|
if (etat.evenement === null) throw new ErreurGeneration('AUCUN_EVENEMENT', { remede: null });
|
||
|
|
if (etat.refus !== null) throw new ErreurGeneration(etat.refus.code, etat.refus.details);
|
||
|
|
if (execution !== null || observable.enCours) throw new ErreurGeneration('GENERATION_EN_COURS', { remede: null });
|
||
|
|
const { charge } = etat;
|
||
|
|
if (!charge.participants.some(({ exclu }) => !exclu)) {
|
||
|
|
throw new ErreurGeneration('AUCUN_PARTICIPANT', { remede: versSection('participants') });
|
||
|
|
}
|
||
|
|
if (charge.tables.length === 0) throw new ErreurGeneration('AUCUNE_TABLE', { remede: versSection('tables') });
|
||
|
|
const configuration = configurationDepuisCharge(charge);
|
||
|
|
let instance;
|
||
|
|
try {
|
||
|
|
instance = normaliser(configuration);
|
||
|
|
} catch (erreur) {
|
||
|
|
if (erreur instanceof ErreurConfiguration) throw new ErreurGeneration(erreur.code, { ...erreur.details, remede: null });
|
||
|
|
throw erreur;
|
||
|
|
}
|
||
|
|
const placesManquantes = nombrePlacesManquantes(instance);
|
||
|
|
if (placesManquantes > 0) {
|
||
|
|
throw new ErreurGeneration('PLACES_MANQUANTES', { placesManquantes, remede: versSection('tables') });
|
||
|
|
}
|
||
|
|
const estimation = estimer(reglages);
|
||
|
|
if (!confirmee && estLongue(reglages, estimation)) {
|
||
|
|
throw new ErreurGeneration('CONFIRMATION_REQUISE', { estimation, remede: null });
|
||
|
|
}
|
||
|
|
return { configuration, instance };
|
||
|
|
}
|
||
|
|
|
||
|
|
// Le calcul confié à l'exécuteur : ses avancements publiés, son annulation
|
||
|
|
// par la séance suivie ; rend les propositions et la durée du calcul. Une
|
||
|
|
// annulation lève ErreurAnnulee, même quand l'exécuteur a rendu un
|
||
|
|
// résultat entre-temps.
|
||
|
|
async function calculer(configuration, reglages) {
|
||
|
|
const debut = horloge.instant();
|
||
|
|
execution = executeur.lancer(configuration, reglages);
|
||
|
|
const desabonner = execution.surAvancement((fait, total) => publier({ fait, total }));
|
||
|
|
const desannuler = seance.surAnnulation(() => annulerAvec('seance'));
|
||
|
|
try {
|
||
|
|
const propositions = await execution.resultat;
|
||
|
|
if (observable.annulee !== null) throw new ErreurAnnulee();
|
||
|
|
return { propositions, dureeMs: horloge.instant() - debut };
|
||
|
|
} finally {
|
||
|
|
desabonner();
|
||
|
|
desannuler();
|
||
|
|
execution = null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Les propositions à enregistrer, par identifiant du moteur croissant :
|
||
|
|
// chacune sans violation des invariants, sinon GENERATION_INVARIANT_VIOLE,
|
||
|
|
// rang compris — la place de la proposition dans la suite des graines du
|
||
|
|
// lancement, qui n'est l'identifiant d'aucune proposition enregistrée ; puis sans
|
||
|
|
// celles dont le plan est celui d'une proposition présente sans dérive, ou
|
||
|
|
// d'une proposition déjà gardée de ce lancement.
|
||
|
|
function trier(propositions, configuration, instance) {
|
||
|
|
const ordonnees = [...propositions].sort((a, b) => a.id - b.id);
|
||
|
|
for (const proposition of ordonnees) {
|
||
|
|
const violations = verifierInvariants(instance, proposition.plan);
|
||
|
|
if (violations.length > 0) {
|
||
|
|
throw new ErreurGeneration('GENERATION_INVARIANT_VIOLE', { rang: proposition.id, violations, remede: null });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const charge = seance.etat().charge;
|
||
|
|
const presents = new Set(
|
||
|
|
charge.propositions.filter((p) => derive(p, charge).length === 0).map((p) => JSON.stringify(planDepuisFichier(p))),
|
||
|
|
);
|
||
|
|
const gardees = [];
|
||
|
|
for (const proposition of ordonnees) {
|
||
|
|
const plan = planRange(proposition, configuration, produitVersion);
|
||
|
|
if (presents.has(plan)) continue;
|
||
|
|
presents.add(plan);
|
||
|
|
gardees.push(proposition);
|
||
|
|
}
|
||
|
|
return { gardees, identiques: ordonnees.length - gardees.length };
|
||
|
|
}
|
||
|
|
|
||
|
|
// Les identifiants que l'enregistrement a donnés : ceux des propositions
|
||
|
|
// de la charge qui n'étaient pas avant le geste, et qui portent la graine,
|
||
|
|
// l'arrêt et l'historique d'une proposition gardée ; croissants.
|
||
|
|
function ajouteesDans(charge, avant, gardees) {
|
||
|
|
const cles = new Set(gardees.map(({ graine: g, arret, historique }) => `${g}|${arret}|${historique}`));
|
||
|
|
return charge.propositions
|
||
|
|
.filter(({ id, graine: g, arret, historique }) => !avant.has(id) && cles.has(`${g}|${arret}|${historique}`))
|
||
|
|
.map(({ id }) => id)
|
||
|
|
.sort((a, b) => a - b);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function generer(reglages, { confirmee = false } = {}) {
|
||
|
|
const { configuration, instance } = exigerLancement(reglages, confirmee);
|
||
|
|
graine = (reglages.graine + 1) % GRAINES;
|
||
|
|
publier({ enCours: true, fait: 0, total: reglages.nombre * reglages.arret, annulee: null });
|
||
|
|
seance.debuterCalcul();
|
||
|
|
try {
|
||
|
|
const { propositions, dureeMs } = await calculer(configuration, reglages);
|
||
|
|
mesure = {
|
||
|
|
dureeMs,
|
||
|
|
mouvements: reglages.nombre * reglages.arret,
|
||
|
|
participants: instance.N,
|
||
|
|
nombre: reglages.nombre,
|
||
|
|
arret: reglages.arret,
|
||
|
|
};
|
||
|
|
const { gardees, identiques } = trier(propositions, configuration, instance);
|
||
|
|
if (gardees.length === 0) return { ajoutees: [], identiques, dureeMs };
|
||
|
|
const avant = new Set(seance.etat().charge.propositions.map(({ id }) => id));
|
||
|
|
await seance.executer('enregistrerGeneration', { propositions: gardees, configuration, produitVersion });
|
||
|
|
return { ajoutees: ajouteesDans(seance.etat().charge, avant, gardees), identiques, dureeMs };
|
||
|
|
} finally {
|
||
|
|
seance.finirCalcul();
|
||
|
|
publier({ enCours: false });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
/** La graine proposée : 1 à chaque ouverture, puis la dernière lancée plus un, modulo 2^32. */
|
||
|
|
grainePropose: () => graine,
|
||
|
|
/** { graine, nombre, arret, historique } : la graine proposée, les trois autres de la charge. */
|
||
|
|
reglagesProposes() {
|
||
|
|
const { nombre, arret, historique } = reglagesDeLaCharge();
|
||
|
|
return { graine, nombre, arret, historique };
|
||
|
|
},
|
||
|
|
/**
|
||
|
|
* Les saisies des quatre champs, chacune lue par lireEntier (NON_ENTIER
|
||
|
|
* {saisie}), puis bornée à son domaine (HORS_DOMAINE {min, max}, max null
|
||
|
|
* sans borne) ; un historique non saisi — absent, null ou blanc — vaut
|
||
|
|
* celui de la charge. Rend { reglages, refus } : reglages null dès
|
||
|
|
* qu'un champ est refusé, refus dans l'ordre des champs.
|
||
|
|
*/
|
||
|
|
valider(saisies) {
|
||
|
|
const reglages = {};
|
||
|
|
const refus = [];
|
||
|
|
for (const [champ, min, max] of CHAMPS) {
|
||
|
|
const saisie = saisies[champ];
|
||
|
|
if (champ === 'historique' && (saisie === undefined || saisie === null || (typeof saisie === 'string' && saisie.trim() === ''))) {
|
||
|
|
reglages.historique = reglagesDeLaCharge().historique;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const lu = lireEntier(saisie);
|
||
|
|
if (lu.refus !== undefined) {
|
||
|
|
refus.push({ champ, code: lu.refus.code, details: lu.refus.details });
|
||
|
|
} else if (lu.valeur < min || (max !== null && lu.valeur > max)) {
|
||
|
|
refus.push({ champ, code: 'HORS_DOMAINE', details: { min, max, remede: null } });
|
||
|
|
} else {
|
||
|
|
reglages[champ] = lu.valeur;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return { reglages: refus.length === 0 ? reglages : null, refus };
|
||
|
|
},
|
||
|
|
/**
|
||
|
|
* L'estimation des réglages, proportionnelle aux mouvements de la
|
||
|
|
* dernière mesure : { inconnue: true } sans elle, sinon { secondes,
|
||
|
|
* longue, auDelaDuPlafond, mesure }.
|
||
|
|
*/
|
||
|
|
estimer,
|
||
|
|
/** La mesure du dernier calcul abouti de ce contrôleur, en mémoire ; null avant lui. */
|
||
|
|
derniereGeneration: () => (mesure === null ? null : { ...mesure }),
|
||
|
|
/**
|
||
|
|
* Lance une génération et l'enregistre : { ajoutees, identiques, dureeMs }.
|
||
|
|
* Refuse avant tout lancement, par ErreurGeneration, dans cet ordre :
|
||
|
|
* REGLAGE_HORS_DOMAINE {reglage} ; AUCUN_EVENEMENT ; le refus courant de
|
||
|
|
* la séance ; GENERATION_EN_COURS ;
|
||
|
|
* AUCUN_PARTICIPANT ; AUCUNE_TABLE ; le refus de normaliser ;
|
||
|
|
* PLACES_MANQUANTES ; CONFIRMATION_REQUISE {estimation}, que
|
||
|
|
* confirmee lève. Après la recherche : GENERATION_INVARIANT_VIOLE
|
||
|
|
* {rang, violations}, puis ce
|
||
|
|
* qu'executer lève, tel quel. Annulée : ErreurAnnulee.
|
||
|
|
*/
|
||
|
|
generer,
|
||
|
|
/** Annule la génération en cours, sans résultat partiel ; annulee vaut 'commande'. */
|
||
|
|
annuler: () => annulerAvec('commande'),
|
||
|
|
/**
|
||
|
|
* Le contrat des magasins de Svelte sur { enCours, fait, total, annulee } :
|
||
|
|
* annulee, la cause de la dernière annulation, 'commande' ou 'seance',
|
||
|
|
* null dès le lancement suivant.
|
||
|
|
*/
|
||
|
|
subscribe(fn) {
|
||
|
|
abonnes.add(fn);
|
||
|
|
fn(observable);
|
||
|
|
return () => {
|
||
|
|
abonnes.delete(fn);
|
||
|
|
};
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|