discovery-media-player 0.1.116 → 0.1.118

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.
@@ -53,8 +53,18 @@ function creerDb(env) {
53
53
  );
54
54
  }
55
55
  const methode = String(options.method || "GET").toUpperCase();
56
+ // ⚠️ UN ABANDON RÉEL, PAS UNE COURSE DE PROMESSES. Sans signal, une base qui ne répond pas
57
+ // immobilise la requête, sa socket ET la place d'admission du cache jusqu'à ce que la plateforme
58
+ // tue la fonction : le plafond d'admission se remplit alors et n'est jamais rendu, ce qui
59
+ // transforme un ralentissement en refus général. Une course `Promise.race` ne suffirait pas —
60
+ // elle rendrait la main sans ANNULER le fetch, donc sans libérer quoi que ce soit. L'hôte peut
61
+ // fournir son propre `signal` (opérations longues : purges, transferts). (Audit externe.)
62
+ const delai = Number(options.timeoutMs) > 0 ? Number(options.timeoutMs) : 15000;
63
+ const signal = options.signal || (typeof AbortSignal !== "undefined" && AbortSignal.timeout
64
+ ? AbortSignal.timeout(delai) : undefined);
56
65
  const r = await fetch(`${url}/rest/v1/${chemin}`, {
57
66
  method: methode,
67
+ ...(signal ? { signal } : {}),
58
68
  headers: {
59
69
  apikey: cle,
60
70
  Authorization: `Bearer ${cle}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discovery-media-player",
3
- "version": "0.1.116",
3
+ "version": "0.1.118",
4
4
  "description": "Self-hosted document viewer: per-recipient tracked links, reading analytics, live presentation. The core knows nothing about the application hosting it — everything it borrows arrives through an injected context.",
5
5
  "keywords": [
6
6
  "pdf-viewer",
@@ -4,4 +4,4 @@
4
4
  // Injecté par api/doc.js dans un <script nonce=…> de la visionneuse. Le code navigateur du
5
5
  // player vit en modules TypeScript testés sous player/src/ — c'est ici qu'il atterrit.
6
6
  "use strict";
7
- module.exports = { PLAYER_BROWSER_JS: "\"use strict\";var Player=(()=>{var C=Object.defineProperty,We=Object.defineProperties,Ve=Object.getOwnPropertyDescriptor,ze=Object.getOwnPropertyDescriptors,Fe=Object.getOwnPropertyNames,ve=Object.getOwnPropertySymbols;var we=Object.prototype.hasOwnProperty,Be=Object.prototype.propertyIsEnumerable;var Me=(e,n,t)=>n in e?C(e,n,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[n]=t,K=(e,n)=>{for(var t in n||(n={}))we.call(n,t)&&Me(e,t,n[t]);if(ve)for(var t of ve(n))Be.call(n,t)&&Me(e,t,n[t]);return e},xe=(e,n)=>We(e,ze(n));var M=(e,n)=>{for(var t in n)C(e,t,{get:n[t],enumerable:!0})},je=(e,n,t,r)=>{if(n&&typeof n==\"object\"||typeof n==\"function\")for(let o of Fe(n))!we.call(e,o)&&o!==t&&C(e,o,{get:()=>n[o],enumerable:!(r=Ve(n,o))||r.enumerable});return e};var Ge=e=>je(C({},\"__esModule\",{value:!0}),e);var Kn={};M(Kn,{bridge:()=>N,cadence:()=>L,chat:()=>U,live:()=>H,presentation:()=>D,presentationState:()=>V,tracking:()=>$,viewer:()=>W});var N={};M(N,{onHostMessage:()=>tn,onPlayerMessage:()=>nn,parseHostMessage:()=>Te,parsePlayerMessage:()=>Ee,sendToHost:()=>Xe,sendToPlayer:()=>Je,toWire:()=>Z});var Y=\"3dd-doc-\",Ke=/^[A-Za-z0-9_-]{1,64}$/,Ye=[\"close\",\"share\",\"embed-ready\",\"present-left\",\"present-denied\"],Ze=[\"embed-denied\"],qe=[\"present-invite\",\"present-handover\",\"present-switch\"],Qe=[\"handover-done\"];function Z(e){let n={type:Y+e.type};return\"slug\"in e&&e.slug&&(n.slug=e.slug),\"reason\"in e&&e.reason&&(n.reason=String(e.reason).slice(0,40)),n}function _e(e,n,t){if(!e||typeof e!=\"object\")return null;let r=e.type;if(typeof r!=\"string\"||!r.startsWith(Y))return null;let o=r.slice(Y.length);if(n.includes(o))return{type:o};if(Ze.includes(o)){let a=e.reason;return{type:o,reason:/^[a-z-]{1,40}$/.test(String(a||\"\"))?String(a):\"unknown\"}}if(!t.includes(o))return null;let s=e.slug;return typeof s!=\"string\"||!Ke.test(s)?null:{type:o,slug:s}}function Ee(e){return _e(e,Ye,qe)}function Te(e){return _e(e,Qe,[])}function Xe(e,n){try{let t=n||(typeof window!=\"undefined\"?window.parent:null);t&&t.postMessage(Z(e),\"*\")}catch(t){}}function Je(e,n){var t;try{(t=e==null?void 0:e.contentWindow)==null||t.postMessage(Z(n),\"*\")}catch(r){}}function en(e,n){var r;if(!e)return!0;let t=(r=e.contentWindow)!=null?r:e;return n===t}function nn(e,n,t){let r=n||(typeof window!=\"undefined\"?window:null);if(!r)return()=>{};let o=s=>{if(!en(t,s.source))return;let a=Ee(s.data);a&&e(a)};return r.addEventListener(\"message\",o),()=>r.removeEventListener(\"message\",o)}function tn(e,n){var s;let t=n||(typeof window!=\"undefined\"?window:null);if(!t)return()=>{};let r=(s=t.parent)!=null?s:null,o=a=>{if(!r||r===t||a.source!==r)return;let l=Te(a.data);l&&e(l)};return t.addEventListener(\"message\",o),()=>t.removeEventListener(\"message\",o)}var $={};M($,{createTracker:()=>pn});var L={};M(L,{PRESENTER_ACTIONS_PER_HOUR:()=>X,PRESENT_CACHE_MS:()=>an,PRESENT_QUOTA_MARGIN:()=>Ne,PRESENT_QUOTA_PER_HOUR:()=>un,PRESENT_READS_PER_HOUR:()=>Ie,PRESENT_READ_BURST:()=>Ae,PRESENT_READ_COALESCE_MS:()=>Ce,PRESENT_RESYNC_MS:()=>Re,PRESENT_SIGNAL_BUDGET_PER_HOUR:()=>sn,PRESENT_WRITE_TIMEOUT_MS:()=>J,READERS_PER_EGRESS:()=>O,RESYNC_READS_PER_HOUR:()=>Pe,SESSION_IDLE_MS:()=>Q,SESSION_INTERVAL_MS:()=>q,SESSION_QUOTA_PER_HOUR:()=>rn,SESSION_WRITES_PER_HOUR:()=>Se,VIEW_EVENTS_PER_READER_HOUR:()=>ke,VIEW_QUOTA_PER_HOUR:()=>on});var q=3e4,Se=Math.ceil(120),O=25,rn=Se*O,ke=720,on=ke*O,Q=18e4,Re=25e3,Pe=Math.ceil(36e5/Re),X=720,Ie=Pe+X,sn=X,Ae=20,J=1e4,Ce=400,an=Ce,Ne=4,un=(Ie+Ae)*O*Ne;var ln=[\"mousemove\",\"mousedown\",\"keydown\",\"wheel\",\"touchstart\",\"pointerdown\"];function cn(){let e=typeof globalThis!=\"undefined\"?globalThis.crypto:void 0;if(e&&typeof e.randomUUID==\"function\")return e.randomUUID().replace(/-/g,\"\");if(e&&typeof e.getRandomValues==\"function\"){let n=e.getRandomValues(new Uint8Array(16));return Array.from(n,t=>t.toString(16).padStart(2,\"0\")).join(\"\")}return Math.random().toString(36).slice(2)+Date.now().toString(36)}function dn(e,n){return t=>{try{let r=JSON.stringify(t),o=n.navigator;return o&&typeof o.sendBeacon==\"function\"?o.sendBeacon(e,new Blob([r],{type:\"application/json\"})):(n.fetch(e,{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:r,keepalive:!0}),!0)}catch(r){return!1}}}function pn(e={}){var be,ye;let n=e.win||(typeof window!=\"undefined\"?window:null),t=e.doc||n&&n.document,r=e.now||(()=>Date.now()),o=e.endpoint||\"/api/doc\",s=(be=e.idleMs)!=null?be:Q,a=(ye=e.sessionEveryMs)!=null?ye:3e4,l=e.slug||\"\",p=e.internal||null,g=e.sessionId||cn(),f=e.send||dn(o,n),c={},u=0,b=0,d=0,w=0,v=null,T=r(),S=!1,z=!1,I=[],De=()=>!!l||!!p,ce=i=>{if(!De())return!1;try{return f(i)!==!1}catch(m){return!1}},F=()=>!!t&&t.visibilityState===\"visible\"&&!S,de=i=>{let m=r()-i,y=Math.max(0,T-i)+s;return Math.max(0,Math.min(m,y))},B=()=>{v!=null&&u>0&&(c[u]=(c[u]||0)+de(v)/1e3),v=F()?r():null},pe=()=>{B(),v=null},j=()=>{v==null&&F()&&(v=r())},G=()=>{let i=0;for(let m in c)i+=c[m];return v!=null&&(i+=de(v)/1e3),Math.round(i)},fe=()=>{let i={};for(let m in c)i[m]=Math.round(c[m]);return i},k=()=>{T=r(),S&&(S=!1,j())},me=(i,m)=>{p||ce({slug:l,event:i,page:m||b,maxPage:d,seconds:G(),sessionId:g})},ge=\"\",A=()=>{B();let i=G(),m=`${i}|${d}|${w}`;if(m===ge)return;let y={event:\"session\",sessionId:g,numPages:w,maxPage:d,totalSeconds:i,pagesTime:fe()};p?(y.internal=!0,y.docId=p.docId,y.email=p.email,y.name=p.name,p.it&&(y.it=p.it)):y.slug=l,ce(y)&&(ge=m)},R=(i,m,y,he)=>{!i||typeof i.addEventListener!=\"function\"||(i.addEventListener(m,y,he),I.push(()=>{i.removeEventListener(m,y,he)}))};return{sessionId:g,totalSeconds:G,maxPage:()=>d,pageTimes:fe,start(){if(z)return;z=!0,me(\"open\",1),u=1,T=r(),v=F()?r():null,R(t,\"visibilitychange\",()=>{t.visibilityState===\"hidden\"?(pe(),A()):(k(),j())}),R(n,\"focus\",()=>{k(),j()});for(let y of ln)R(n,y,k,{passive:!0});R(e.scrollElement,\"scroll\",k,{passive:!0}),R(n,\"beforeunload\",A);let i=n.setInterval(()=>{!S&&r()-T>s&&(S=!0,pe())},5e3),m=n.setInterval(A,a);I.push(()=>{n.clearInterval(i),n.clearInterval(m)})},setPage(i){!i||i===b||(T=r(),B(),u=i,b=i,i>d&&(d=i,me(\"page\",i)))},setPageCount(i){w=i||0},noteActivity:k,flush:A,stop(){for(;I.length;){let i=I.pop();try{i==null||i()}catch(m){}}z=!1}}}var H={};M(H,{STORAGE_KEYS:()=>x,attendeeKey:()=>bn,authorToken:()=>yn,avatarHtml:()=>ee,canModerate:()=>oe,createBudget:()=>En,createFileEcritures:()=>Tn,createScheduler:()=>xn,escapeHtml:()=>h,fetchBorne:()=>_n,flattenPresence:()=>fn,formatMessageBody:()=>ne,initials:()=>Oe,isMentioned:()=>vn,isMine:()=>_,presenceId:()=>Sn,reactorId:()=>re,referenceAuteur:()=>hn,shouldNotify:()=>Mn,unreadLabel:()=>wn});var x={attendeeKey:\"3dd-present-attkey\",authorToken:\"3dd-present-authtoken\",chatMuted:\"3dd-present-mute\",presenceId:\"3dd-present-uid\"};function h(e){return String(e==null?\"\":e).replace(/&/g,\"&amp;\").replace(/</g,\"&lt;\").replace(/>/g,\"&gt;\").replace(/\"/g,\"&quot;\")}function Oe(e){let n=(e||\"?\").trim().split(/\\s+/);return(((n[0]||\"\")[0]||\"?\")+((n[1]||\"\")[0]||\"\")).toUpperCase()}function ee(e,n){return e?`<img src=\"${h(e)}\" alt=\"\">`:h(Oe(n))}function ne(e){let n=h(e);return n=n.replace(/(https?:\\/\\/[^\\s<]+)/g,t=>`<a href=\"${t}\" target=_blank rel=noreferrer class=cm-link>${t}</a>`),n=n.replace(/(^|\\s)@([\\p{L}0-9_'.-]+)/gu,(t,r,o)=>`${r}<span class=cm-mention>@${o}</span>`),n}function fn(e){let n=new Map;for(let t in e||{})for(let r of(e||{})[t]||[]){let o=String(r.uid||r.email||r.name||\"\").toLowerCase()||`_${t}_${n.size}`;(!n.get(o)||r.role===\"presenter\")&&n.set(o,r)}return[...n.values()]}function mn(e,n){try{return e?e.getItem(n):null}catch(t){return null}}function gn(e,n,t){try{e==null||e.setItem(n,t)}catch(r){}}function bn(e,n=\"anon\",t=te){try{if(!e)return`anon-${n}`;let r=e.getItem(x.attendeeKey);if(r)return r;let o=`anon-${t()}`;return e.setItem(x.attendeeKey,o),o}catch(r){return`anon-${n}`}}function te(){let e=typeof globalThis!=\"undefined\"?globalThis.crypto:void 0;if(e&&typeof e.randomUUID==\"function\")return e.randomUUID().replace(/-/g,\"\");if(e&&typeof e.getRandomValues==\"function\"){let n=e.getRandomValues(new Uint8Array(24));return Array.from(n,t=>t.toString(16).padStart(2,\"0\")).join(\"\")}try{console.warn(\"[player] crypto indisponible : jeton d'auteur affaibli\")}catch(n){}return Math.random().toString(36).slice(2)+Math.random().toString(36).slice(2)}function yn(e,n=te){let t=mn(e,x.authorToken);if(t)return t;let r=n();return gn(e,x.authorToken,r),r}async function hn(e,n){let t=n!==void 0?n:typeof globalThis!=\"undefined\"&&globalThis.crypto?globalThis.crypto.subtle:null;if(!t||!e)return\"\";try{let r=async o=>{let s=await t.digest(\"SHA-256\",new TextEncoder().encode(o));return Array.from(new Uint8Array(s),a=>a.toString(16).padStart(2,\"0\")).join(\"\")};return(await r(\"ref:\"+await r(e))).slice(0,16)}catch(r){return\"\"}}function re(e){return String(e&&e.ref||\"\")}function _(e,n){return!e||!n||!e.author_ref||!n.ref?!1:e.author_ref===n.ref}function oe(e){return!!(e&&e.role===\"presenter\")}function vn(e,n){if(!n||!n.name)return!1;let t=(n.name.split(/\\s+/)[0]||\"\").toLowerCase();return t?String(e&&e.body||\"\").toLowerCase().includes(`@${t}`)&&!_(e,n):!1}function Mn(e){let{msg:n,me:t,historyLoaded:r,chatHidden:o}=e;return!r||!n||n.deleted||_(n,t)?!1:o}function wn(e){return!e||e<=0?\"\":e>9?\"9+\":String(e)}function xn(e,n={}){var b;let t=(b=n.minMs)!=null?b:300,r=n.now||(()=>Date.now()),o=n.setTimer||((d,w)=>setTimeout(d,w)),s=n.clearTimer||(d=>clearTimeout(d)),a=!1,l=!1,p=-1/0,g=null,f=!1;function c(){if(f||a){l=!0;return}let d=t-(r()-p);if(d>0){u(d);return}a=!0,l=!1,p=r();let w=!1;e(()=>{w||(w=!0,a=!1,l&&!f&&c())})}function u(d){f||g!==null||(g=o(()=>{g=null,c()},Math.max(0,d)))}return{signaler(){f||c()},maintenant(){f||(p=-1/0,c())},arreter(){f=!0,g!==null&&(s(g),g=null)}}}function _n(e,n,t=J,r={}){let o=r.fetch||(typeof fetch==\"function\"?fetch:void 0);if(!o)return Promise.reject(new Error(\"fetch indisponible\"));let s=r.setTimer||((u,b)=>setTimeout(u,b)),a=r.clearTimer||(u=>clearTimeout(u)),l=null;try{l=new AbortController}catch(u){}let p=l?xe(K({},n||{}),{signal:l.signal}):K({},n||{}),g=null,f=new Promise((u,b)=>{g=s(()=>{try{l==null||l.abort()}catch(d){}b(new Error(\"delai\"))},t)}),c=()=>{try{a(g)}catch(u){}};return Promise.race([o(e,p),f]).then(u=>(c(),u),u=>{throw c(),u})}function En(e){let n=e.now||(()=>Date.now()),t=Math.max(1,e.rafale),r=t,o=n();return{prendre(){let s=n();return r=Math.min(t,r+(s-o)*e.parHeure/36e5),o=s,r<1?!1:(r-=1,!0)},restants:()=>r}}function Tn(e={}){var g;let n=(g=e.minMs)!=null?g:0,t=e.now||(()=>Date.now()),r=e.setTimer||((f,c)=>setTimeout(f,c)),o=new Map,s=!1,a=-1/0,l=!1;function p(){if(s||o.size===0)return;let f=n-(t()-a);if(f>0){if(l)return;l=!0,r(()=>{l=!1,p()},f);return}let c=o.keys().next().value,u=o.get(c);o.delete(c),s=!0,a=t(),Promise.resolve().then(u.tache).then(b=>u.regler(b),()=>u.regler(null)).then(()=>{s=!1,p()})}return{poser(f,c){return new Promise(u=>{let b=o.get(f);o.set(f,{tache:c,regler:d=>{b&&b.regler(d),u(d)}}),p()})},enAttente:()=>o.size}}function Sn(e,n=te){try{if(e){let t=e.getItem(x.presenceId);if(t)return t;let r=`p-${n()}`;return e.setItem(x.presenceId,r),r}}catch(t){}return`p-${n()}`}var U={};M(U,{messageClassName:()=>Rn,renderActions:()=>He,renderAttachment:()=>$e,renderMessage:()=>kn,renderReactions:()=>Le});function Le(e,n){let t=e&&e.reactions||{},r=re(n),o=\"\";for(let s in t){let a=t[s]||[];if(!a.length)continue;let l=a.indexOf(r)>=0;o+=`<button class=\"re-chip${l?\" on\":\"\"}\" data-e=\"${h(s)}\">${h(s)} ${a.length}</button>`}return o}function $e(e){if(!e||!e.url)return\"\";let n=h(e.url);if(e.kind===\"image\")return`<a class=cm-att href=\"${n}\" target=_blank rel=noreferrer><img src=\"${n}\" alt=\"\" loading=lazy></a>`;let t=h(e.name||\"Document.pdf\");return`<a class=cm-att-pdf href=\"${n}\" target=_blank rel=noreferrer data-pdf=\"${n}\"><span class=cm-att-ph></span><span class=cm-pdflabel>${t}</span></a>`}function He(e,n){let t=_(e,n.me),r=`<button class=cm-react title=\"R\\xE9agir\">${n.reactIcon||\"\"}</button><button class=cm-reply title=\"R\\xE9pondre\">\\u21A9</button>`;return t&&(r+='<button class=cm-edit title=\"Modifier\">\\u270E</button>'),(t||oe(n.me))&&(r+='<button class=cm-del-btn title=\"Supprimer\">\\u{1F5D1}</button>'),r}function kn(e,n){if(e.deleted)return'<span class=a></span><span class=b><div class=\"txt cm-del\">Message supprim\\xE9</div></span>';let t=e.reply_to?`<div class=cm-q><b>${h(e.reply_name||\"\")}</b> ${h(e.reply_text||\"\")}</div>`:\"\",r=e.edited?\" <span class=cm-ed>(modifi\\xE9)</span>\":\"\",o=e.body?`<div class=txt>${ne(e.body)}${r}</div>`:\"\",s=e.is_presenter?\" <span class=tag>pr\\xE9sentateur</span>\":\"\";return`<span class=a>${ee(e.author_avatar,e.author_name)}</span><span class=b><div class=who><b>${h(e.author_name||\"Invit\\xE9\")}</b>${s}</div>`+t+o+$e(e.attachment)+`<div class=cm-re>${Le(e,n.me)}</div></span><span class=cm-act>${He(e,n)}</span>`}function Rn(e,n,t){return\"cm\"+(_(e,n)?\" me\":\"\")+(e.deleted?\" isdel\":\"\")+(t?\" mentioned\":\"\")}var D={};M(D,{DEFAULT_CENTER:()=>ae,DEFAULT_ZOOM:()=>ue,MAP_TYPES:()=>P,cycleMapType:()=>Pn,initialMapContent:()=>An,mapTypeLabel:()=>In,sanitizeContent:()=>le});var P=[\"roadmap\",\"satellite\",\"hybrid\"],ae=[46.6,2.5],ue=6;function E(e){let n=Number(e);return Number.isFinite(n)?n:null}function se(e){if(!Array.isArray(e))return null;let n=E(e[0]),t=E(e[1]);return n!=null&&t!=null?[n,t]:null}var ie=(e,n,t)=>Math.max(n,Math.min(t,e));function le(e){if(!e||typeof e!=\"object\")return null;let n=e;if(n.kind===\"pdf\"||n.kind==null)return null;if(n.kind===\"streetview\"){let t=se(n.position);if(!t)return null;let r=n.pov&&typeof n.pov==\"object\"?n.pov:null,o=r?{heading:E(r.heading)||0,pitch:ie(E(r.pitch)||0,-90,90)}:{heading:0,pitch:0};return{kind:\"streetview\",position:t,pov:o,zoom:ie(E(n.zoom)||1,0,5)}}return n.kind!==\"map\"?null:{kind:\"map\",center:se(n.center)||[...ae],zoom:ie(Math.trunc(E(n.zoom)||ue),1,21),marker:se(n.marker),mapType:P.includes(n.mapType)?n.mapType:null,label:String(n.label||\"\").slice(0,160)||null}}function Pn(e){let n=P.indexOf(e);return P[(n+1)%P.length]}function In(e){return e===\"roadmap\"?\"\\u{1F6F0} Satellite\":e===\"satellite\"?\"\\u{1F5FA} Hybride\":\"\\u{1F5FA} Plan\"}function An(){return{kind:\"map\",center:[...ae],zoom:ue,marker:null,mapType:null,label:null}}var W={};M(W,{CURRENT_PAGE_MARGIN:()=>Hn,DEFAULT_ASPECT:()=>On,MAX_ZOOM:()=>Nn,MIN_PAGE_WIDTH:()=>Ln,MIN_ZOOM:()=>Cn,PRERENDER_MARGIN:()=>$n,arrowState:()=>Wn,averageColor:()=>Fn,clampPage:()=>Ue,clampZoom:()=>Un,fitWidth:()=>zn,isImageDocument:()=>Bn,progressPercent:()=>Dn});var Cn=.5,Nn=3,On=1.35,Ln=280,$n=\"500px 0px\",Hn=\"-48% 0px -48% 0px\";function Un(e){let n=Math.round((Number(e)||0)*10)/10;return Math.max(.5,Math.min(3,n))}function Ue(e,n){return Math.max(1,Math.min(n||1,Number(e)||1))}function Dn(e,n){return n?Math.round(Ue(e,n)/n*100):0}function Wn(e,n){return{prevDisabled:e<=1,nextDisabled:!!n&&e>=n}}var Vn=28;function zn(e){var a;let n=(a=e.zoom)!=null?a:1,t=Math.max(280,e.containerWidth||900)*n;if(!e.onePage)return t;let r=e.aspect||1.35,s=((e.containerHeight||600)-Vn-(e.overlap||0)-(e.reserve||0))/r;return s>120?Math.min(t,s):t}function Fn(e){let n=e.length/4;if(!n)return\"rgb(0,0,0)\";let t=0,r=0,o=0;for(let s=0;s<e.length;s+=4)t+=e[s],r+=e[s+1],o+=e[s+2];return`rgb(${Math.round(t/n)},${Math.round(r/n)},${Math.round(o/n)})`}function Bn(e,n){return/[.](png|jpe?g|webp|gif|avif)($|[?])/i.test(String(e||n||\"\"))}var V={};M(V,{presentationTransition:()=>jn,switchDocUrl:()=>Gn});function jn(e,n={}){if(!e)return[{kind:\"nothing\"}];if(e.active===!1)return[{kind:\"ended\"}];let t=le(e.content);if(t)return[{kind:\"show-map\",content:t}];let r=[{kind:\"leave-map\"}];return e.file_url&&e.file_url!==n.docUrl?(r.push({kind:\"switch-doc\",url:e.file_url,name:e.file_name||void 0,title:e.doc_title||void 0,updatedAt:e.updated_at||void 0}),r):(e.current_page&&r.push({kind:\"show-page\",page:e.current_page}),r)}function Gn(e,n){let t=encodeURIComponent(n||String(Date.now()));return`${e}${e.includes(\"?\")?\"&\":\"?\"}v=${t}`}return Ge(Kn);})();", SOURCE_HASH: "9b51a94c89b8d54f" };
7
+ module.exports = { PLAYER_BROWSER_JS: "\"use strict\";var Player=(()=>{var C=Object.defineProperty,We=Object.defineProperties,Ve=Object.getOwnPropertyDescriptor,ze=Object.getOwnPropertyDescriptors,Fe=Object.getOwnPropertyNames,ve=Object.getOwnPropertySymbols;var we=Object.prototype.hasOwnProperty,Be=Object.prototype.propertyIsEnumerable;var Me=(e,n,t)=>n in e?C(e,n,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[n]=t,K=(e,n)=>{for(var t in n||(n={}))we.call(n,t)&&Me(e,t,n[t]);if(ve)for(var t of ve(n))Be.call(n,t)&&Me(e,t,n[t]);return e},xe=(e,n)=>We(e,ze(n));var M=(e,n)=>{for(var t in n)C(e,t,{get:n[t],enumerable:!0})},je=(e,n,t,r)=>{if(n&&typeof n==\"object\"||typeof n==\"function\")for(let o of Fe(n))!we.call(e,o)&&o!==t&&C(e,o,{get:()=>n[o],enumerable:!(r=Ve(n,o))||r.enumerable});return e};var Ge=e=>je(C({},\"__esModule\",{value:!0}),e);var Kn={};M(Kn,{bridge:()=>N,cadence:()=>L,chat:()=>U,live:()=>H,presentation:()=>D,presentationState:()=>V,tracking:()=>$,viewer:()=>W});var N={};M(N,{onHostMessage:()=>tn,onPlayerMessage:()=>nn,parseHostMessage:()=>Te,parsePlayerMessage:()=>Ee,sendToHost:()=>Xe,sendToPlayer:()=>Je,toWire:()=>Z});var Y=\"3dd-doc-\",Ke=/^[A-Za-z0-9_-]{1,64}$/,Ye=[\"close\",\"share\",\"embed-ready\",\"present-left\",\"present-denied\"],Ze=[\"embed-denied\"],qe=[\"present-invite\",\"present-handover\",\"present-switch\"],Qe=[\"handover-done\"];function Z(e){let n={type:Y+e.type};return\"slug\"in e&&e.slug&&(n.slug=e.slug),\"reason\"in e&&e.reason&&(n.reason=String(e.reason).slice(0,40)),n}function _e(e,n,t){if(!e||typeof e!=\"object\")return null;let r=e.type;if(typeof r!=\"string\"||!r.startsWith(Y))return null;let o=r.slice(Y.length);if(n.includes(o))return{type:o};if(Ze.includes(o)){let a=e.reason;return{type:o,reason:/^[a-z-]{1,40}$/.test(String(a||\"\"))?String(a):\"unknown\"}}if(!t.includes(o))return null;let s=e.slug;return typeof s!=\"string\"||!Ke.test(s)?null:{type:o,slug:s}}function Ee(e){return _e(e,Ye,qe)}function Te(e){return _e(e,Qe,[])}function Xe(e,n){try{let t=n||(typeof window!=\"undefined\"?window.parent:null);t&&t.postMessage(Z(e),\"*\")}catch(t){}}function Je(e,n){var t;try{(t=e==null?void 0:e.contentWindow)==null||t.postMessage(Z(n),\"*\")}catch(r){}}function en(e,n){var r;if(!e)return!0;let t=(r=e.contentWindow)!=null?r:e;return n===t}function nn(e,n,t){let r=n||(typeof window!=\"undefined\"?window:null);if(!r)return()=>{};let o=s=>{if(!en(t,s.source))return;let a=Ee(s.data);a&&e(a)};return r.addEventListener(\"message\",o),()=>r.removeEventListener(\"message\",o)}function tn(e,n){var s;let t=n||(typeof window!=\"undefined\"?window:null);if(!t)return()=>{};let r=(s=t.parent)!=null?s:null,o=a=>{if(!r||r===t||a.source!==r)return;let l=Te(a.data);l&&e(l)};return t.addEventListener(\"message\",o),()=>t.removeEventListener(\"message\",o)}var $={};M($,{createTracker:()=>pn});var L={};M(L,{PRESENTER_ACTIONS_PER_HOUR:()=>X,PRESENT_CACHE_MS:()=>an,PRESENT_QUOTA_MARGIN:()=>Ne,PRESENT_QUOTA_PER_HOUR:()=>un,PRESENT_READS_PER_HOUR:()=>Ie,PRESENT_READ_BURST:()=>Ae,PRESENT_READ_COALESCE_MS:()=>Ce,PRESENT_RESYNC_MS:()=>Re,PRESENT_SIGNAL_BUDGET_PER_HOUR:()=>sn,PRESENT_WRITE_TIMEOUT_MS:()=>J,READERS_PER_EGRESS:()=>O,RESYNC_READS_PER_HOUR:()=>Pe,SESSION_IDLE_MS:()=>Q,SESSION_INTERVAL_MS:()=>q,SESSION_QUOTA_PER_HOUR:()=>rn,SESSION_WRITES_PER_HOUR:()=>Se,VIEW_EVENTS_PER_READER_HOUR:()=>ke,VIEW_QUOTA_PER_HOUR:()=>on});var q=3e4,Se=Math.ceil(120),O=25,rn=Se*O,ke=720,on=ke*O,Q=18e4,Re=25e3,Pe=Math.ceil(36e5/Re),X=720,Ie=Pe+X,sn=X,Ae=20,J=1e4,Ce=400,an=Ce,Ne=4,un=(Ie+Ae)*O*Ne;var ln=[\"mousemove\",\"mousedown\",\"keydown\",\"wheel\",\"touchstart\",\"pointerdown\"];function cn(){let e=typeof globalThis!=\"undefined\"?globalThis.crypto:void 0;if(e&&typeof e.randomUUID==\"function\")return e.randomUUID().replace(/-/g,\"\");if(e&&typeof e.getRandomValues==\"function\"){let n=e.getRandomValues(new Uint8Array(16));return Array.from(n,t=>t.toString(16).padStart(2,\"0\")).join(\"\")}return Math.random().toString(36).slice(2)+Date.now().toString(36)}function dn(e,n){return t=>{try{let r=JSON.stringify(t),o=n.navigator;return o&&typeof o.sendBeacon==\"function\"?o.sendBeacon(e,new Blob([r],{type:\"application/json\"})):(n.fetch(e,{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:r,keepalive:!0}),!0)}catch(r){return!1}}}function pn(e={}){var be,ye;let n=e.win||(typeof window!=\"undefined\"?window:null),t=e.doc||n&&n.document,r=e.now||(()=>Date.now()),o=e.endpoint||\"/api/doc\",s=(be=e.idleMs)!=null?be:Q,a=(ye=e.sessionEveryMs)!=null?ye:3e4,l=e.slug||\"\",p=e.internal||null,g=e.sessionId||cn(),f=e.send||dn(o,n),c={},u=0,b=0,d=0,w=0,v=null,T=r(),S=!1,z=!1,I=[],De=()=>!!l||!!p,ce=i=>{if(!De())return!1;try{return f(i)!==!1}catch(m){return!1}},F=()=>!!t&&t.visibilityState===\"visible\"&&!S,de=i=>{let m=r()-i,y=Math.max(0,T-i)+s;return Math.max(0,Math.min(m,y))},B=()=>{v!=null&&u>0&&(c[u]=(c[u]||0)+de(v)/1e3),v=F()?r():null},pe=()=>{B(),v=null},j=()=>{v==null&&F()&&(v=r())},G=()=>{let i=0;for(let m in c)i+=c[m];return v!=null&&(i+=de(v)/1e3),Math.round(i)},fe=()=>{let i={};for(let m in c)i[m]=Math.round(c[m]);return i},k=()=>{T=r(),S&&(S=!1,j())},me=(i,m)=>{p||ce({slug:l,event:i,page:m||b,maxPage:d,seconds:G(),sessionId:g})},ge=\"\",A=()=>{B();let i=G(),m=`${i}|${d}|${w}`;if(m===ge)return;let y={event:\"session\",sessionId:g,numPages:w,maxPage:d,totalSeconds:i,pagesTime:fe()};p?(y.internal=!0,y.docId=p.docId,y.email=p.email,y.name=p.name,p.it&&(y.it=p.it)):y.slug=l,ce(y)&&(ge=m)},R=(i,m,y,he)=>{!i||typeof i.addEventListener!=\"function\"||(i.addEventListener(m,y,he),I.push(()=>{i.removeEventListener(m,y,he)}))};return{sessionId:g,totalSeconds:G,maxPage:()=>d,pageTimes:fe,start(){if(z)return;z=!0,me(\"open\",1),u=1,T=r(),v=F()?r():null,R(t,\"visibilitychange\",()=>{t.visibilityState===\"hidden\"?(pe(),A()):(k(),j())}),R(n,\"focus\",()=>{k(),j()});for(let y of ln)R(n,y,k,{passive:!0});R(e.scrollElement,\"scroll\",k,{passive:!0}),R(n,\"beforeunload\",A);let i=n.setInterval(()=>{!S&&r()-T>s&&(S=!0,pe())},5e3),m=n.setInterval(A,a);I.push(()=>{n.clearInterval(i),n.clearInterval(m)})},setPage(i){!i||i===b||(T=r(),B(),u=i,b=i,i>d&&(d=i,me(\"page\",i)))},setPageCount(i){w=i||0},noteActivity:k,flush:A,stop(){for(;I.length;){let i=I.pop();try{i==null||i()}catch(m){}}z=!1}}}var H={};M(H,{STORAGE_KEYS:()=>x,attendeeKey:()=>bn,authorToken:()=>yn,avatarHtml:()=>ee,canModerate:()=>oe,createBudget:()=>En,createFileEcritures:()=>Tn,createScheduler:()=>xn,escapeHtml:()=>h,fetchBorne:()=>_n,flattenPresence:()=>fn,formatMessageBody:()=>ne,initials:()=>Oe,isMentioned:()=>vn,isMine:()=>_,presenceId:()=>Sn,reactorId:()=>re,referenceAuteur:()=>hn,shouldNotify:()=>Mn,unreadLabel:()=>wn});var x={attendeeKey:\"3dd-present-attkey\",authorToken:\"3dd-present-authtoken\",chatMuted:\"3dd-present-mute\",presenceId:\"3dd-present-uid\"};function h(e){return String(e==null?\"\":e).replace(/&/g,\"&amp;\").replace(/</g,\"&lt;\").replace(/>/g,\"&gt;\").replace(/\"/g,\"&quot;\")}function Oe(e){let n=(e||\"?\").trim().split(/\\s+/);return(((n[0]||\"\")[0]||\"?\")+((n[1]||\"\")[0]||\"\")).toUpperCase()}function ee(e,n){return e?`<img src=\"${h(e)}\" alt=\"\">`:h(Oe(n))}function ne(e){let n=h(e);return n=n.replace(/(https?:\\/\\/[^\\s<]+)/g,t=>`<a href=\"${t}\" target=_blank rel=noreferrer class=cm-link>${t}</a>`),n=n.replace(/(^|\\s)@([\\p{L}0-9_'.-]+)/gu,(t,r,o)=>`${r}<span class=cm-mention>@${o}</span>`),n}function fn(e){let n=new Map;for(let t in e||{})for(let r of(e||{})[t]||[]){let o=String(r.uid||r.email||r.name||\"\").toLowerCase()||`_${t}_${n.size}`;(!n.get(o)||r.role===\"presenter\")&&n.set(o,r)}return[...n.values()]}function mn(e,n){try{return e?e.getItem(n):null}catch(t){return null}}function gn(e,n,t){try{e==null||e.setItem(n,t)}catch(r){}}function bn(e,n=\"anon\",t=te){try{if(!e)return`anon-${n}`;let r=e.getItem(x.attendeeKey);if(r)return r;let o=`anon-${t()}`;return e.setItem(x.attendeeKey,o),o}catch(r){return`anon-${n}`}}function te(){let e=typeof globalThis!=\"undefined\"?globalThis.crypto:void 0;if(e&&typeof e.randomUUID==\"function\")return e.randomUUID().replace(/-/g,\"\");if(e&&typeof e.getRandomValues==\"function\"){let n=e.getRandomValues(new Uint8Array(24));return Array.from(n,t=>t.toString(16).padStart(2,\"0\")).join(\"\")}try{console.warn(\"[player] crypto indisponible : jeton d'auteur affaibli\")}catch(n){}return Math.random().toString(36).slice(2)+Math.random().toString(36).slice(2)}function yn(e,n=te){let t=mn(e,x.authorToken);if(t)return t;let r=n();return gn(e,x.authorToken,r),r}async function hn(e,n){let t=n!==void 0?n:typeof globalThis!=\"undefined\"&&globalThis.crypto?globalThis.crypto.subtle:null;if(!t||!e)return\"\";try{let r=async o=>{let s=await t.digest(\"SHA-256\",new TextEncoder().encode(o));return Array.from(new Uint8Array(s),a=>a.toString(16).padStart(2,\"0\")).join(\"\")};return(await r(\"ref:\"+await r(e))).slice(0,16)}catch(r){return\"\"}}function re(e){return String(e&&e.ref||\"\")}function _(e,n){return!e||!n||!e.author_ref||!n.ref?!1:e.author_ref===n.ref}function oe(e){return!!(e&&e.role===\"presenter\")}function vn(e,n){if(!n||!n.name)return!1;let t=(n.name.split(/\\s+/)[0]||\"\").toLowerCase();return t?String(e&&e.body||\"\").toLowerCase().includes(`@${t}`)&&!_(e,n):!1}function Mn(e){let{msg:n,me:t,historyLoaded:r,chatHidden:o}=e;return!r||!n||n.deleted||_(n,t)?!1:o}function wn(e){return!e||e<=0?\"\":e>9?\"9+\":String(e)}function xn(e,n={}){var b;let t=(b=n.minMs)!=null?b:300,r=n.now||(()=>Date.now()),o=n.setTimer||((d,w)=>setTimeout(d,w)),s=n.clearTimer||(d=>clearTimeout(d)),a=!1,l=!1,p=-1/0,g=null,f=!1;function c(){if(f||a){l=!0;return}let d=t-(r()-p);if(d>0){u(d);return}a=!0,l=!1,p=r();let w=!1;e(()=>{w||(w=!0,a=!1,l&&!f&&c())})}function u(d){f||g!==null||(g=o(()=>{g=null,c()},Math.max(0,d)))}return{signaler(){f||c()},maintenant(){f||(p=-1/0,c())},arreter(){f=!0,g!==null&&(s(g),g=null)}}}function _n(e,n,t=J,r={}){let o=r.fetch||(typeof fetch==\"function\"?fetch:void 0);if(!o)return Promise.reject(new Error(\"fetch indisponible\"));let s=r.setTimer||((u,b)=>setTimeout(u,b)),a=r.clearTimer||(u=>clearTimeout(u)),l=null;try{l=new AbortController}catch(u){}let p=l?xe(K({},n||{}),{signal:l.signal}):K({},n||{}),g=null,f=new Promise((u,b)=>{g=s(()=>{try{l==null||l.abort()}catch(d){}b(new Error(\"delai\"))},t)}),c=()=>{try{a(g)}catch(u){}};return Promise.race([o(e,p),f]).then(u=>(c(),u),u=>{throw c(),u})}function En(e){let n=e.now||(()=>Date.now()),t=Math.max(1,e.rafale),r=t,o=n();return{prendre(){let s=n();return r=Math.min(t,r+(s-o)*e.parHeure/36e5),o=s,r<1?!1:(r-=1,!0)},restants:()=>r}}function Tn(e={}){var g;let n=(g=e.minMs)!=null?g:0,t=e.now||(()=>Date.now()),r=e.setTimer||((f,c)=>setTimeout(f,c)),o=new Map,s=!1,a=-1/0,l=!1;function p(){if(s||o.size===0)return;let f=n-(t()-a);if(f>0){if(l)return;l=!0,r(()=>{l=!1,p()},f);return}let c=o.keys().next().value,u=o.get(c);o.delete(c),s=!0,a=t(),Promise.resolve().then(u.tache).then(b=>u.regler(b),()=>u.regler(null)).then(()=>{s=!1,p()})}return{poser(f,c){return new Promise(u=>{let b=o.get(f);o.set(f,{tache:c,regler:d=>{b&&b.regler(d),u(d)}}),p()})},enAttente:()=>o.size}}function Sn(e,n=te){try{if(e){let t=e.getItem(x.presenceId);if(t)return t;let r=`p-${n()}`;return e.setItem(x.presenceId,r),r}}catch(t){}return`p-${n()}`}var U={};M(U,{messageClassName:()=>Rn,renderActions:()=>He,renderAttachment:()=>$e,renderMessage:()=>kn,renderReactions:()=>Le});function Le(e,n){let t=e&&e.reactions||{},r=re(n),o=\"\";for(let s in t){let a=t[s]||[];if(!a.length)continue;let l=a.indexOf(r)>=0;o+=`<button class=\"re-chip${l?\" on\":\"\"}\" data-e=\"${h(s)}\">${h(s)} ${a.length}</button>`}return o}function $e(e){if(!e||!e.url)return\"\";let n=h(e.url);if(e.kind===\"image\")return`<a class=cm-att href=\"${n}\" target=_blank rel=noreferrer><img src=\"${n}\" alt=\"\" loading=lazy></a>`;let t=h(e.name||\"Document.pdf\");return`<a class=cm-att-pdf href=\"${n}\" target=_blank rel=noreferrer data-pdf=\"${n}\"><span class=cm-att-ph></span><span class=cm-pdflabel>${t}</span></a>`}function He(e,n){let t=_(e,n.me),r=`<button class=cm-react title=\"R\\xE9agir\">${n.reactIcon||\"\"}</button><button class=cm-reply title=\"R\\xE9pondre\">\\u21A9</button>`;return t&&(r+='<button class=cm-edit title=\"Modifier\">\\u270E</button>'),(t||oe(n.me))&&(r+='<button class=cm-del-btn title=\"Supprimer\">\\u{1F5D1}</button>'),r}function kn(e,n){if(e.deleted)return'<span class=a></span><span class=b><div class=\"txt cm-del\">Message supprim\\xE9</div></span>';let t=e.reply_to?`<div class=cm-q><b>${h(e.reply_name||\"\")}</b> ${h(e.reply_text||\"\")}</div>`:\"\",r=e.edited?\" <span class=cm-ed>(modifi\\xE9)</span>\":\"\",o=e.body?`<div class=txt>${ne(e.body)}${r}</div>`:\"\",s=e.is_presenter?\" <span class=tag>pr\\xE9sentateur</span>\":\"\";return`<span class=a>${ee(e.author_avatar,e.author_name)}</span><span class=b><div class=who><b>${h(e.author_name||\"Invit\\xE9\")}</b>${s}</div>`+t+o+$e(e.attachment)+`<div class=cm-re>${Le(e,n.me)}</div></span><span class=cm-act>${He(e,n)}</span>`}function Rn(e,n,t){return\"cm\"+(_(e,n)?\" me\":\"\")+(e.deleted?\" isdel\":\"\")+(t?\" mentioned\":\"\")}var D={};M(D,{DEFAULT_CENTER:()=>ae,DEFAULT_ZOOM:()=>ue,MAP_TYPES:()=>P,cycleMapType:()=>Pn,initialMapContent:()=>An,mapTypeLabel:()=>In,sanitizeContent:()=>le});var P=[\"roadmap\",\"satellite\",\"hybrid\"],ae=[46.6,2.5],ue=6;function E(e){let n=Number(e);return Number.isFinite(n)?n:null}function se(e){if(!Array.isArray(e))return null;let n=E(e[0]),t=E(e[1]);return n!=null&&t!=null?[n,t]:null}var ie=(e,n,t)=>Math.max(n,Math.min(t,e));function le(e){if(!e||typeof e!=\"object\")return null;let n=e;if(n.kind===\"pdf\"||n.kind==null)return null;if(n.kind===\"streetview\"){let t=se(n.position);if(!t)return null;let r=n.pov&&typeof n.pov==\"object\"?n.pov:null,o=r?{heading:E(r.heading)||0,pitch:ie(E(r.pitch)||0,-90,90)}:{heading:0,pitch:0};return{kind:\"streetview\",position:t,pov:o,zoom:ie(E(n.zoom)||1,0,5)}}return n.kind!==\"map\"?null:{kind:\"map\",center:se(n.center)||[...ae],zoom:ie(Math.trunc(E(n.zoom)||ue),1,21),marker:se(n.marker),mapType:P.includes(n.mapType)?n.mapType:null,label:String(n.label||\"\").slice(0,160)||null}}function Pn(e){let n=P.indexOf(e);return P[(n+1)%P.length]}function In(e){return e===\"roadmap\"?\"\\u{1F6F0} Satellite\":e===\"satellite\"?\"\\u{1F5FA} Hybride\":\"\\u{1F5FA} Plan\"}function An(){return{kind:\"map\",center:[...ae],zoom:ue,marker:null,mapType:null,label:null}}var W={};M(W,{CURRENT_PAGE_MARGIN:()=>Hn,DEFAULT_ASPECT:()=>On,MAX_ZOOM:()=>Nn,MIN_PAGE_WIDTH:()=>Ln,MIN_ZOOM:()=>Cn,PRERENDER_MARGIN:()=>$n,arrowState:()=>Wn,averageColor:()=>Fn,clampPage:()=>Ue,clampZoom:()=>Un,fitWidth:()=>zn,isImageDocument:()=>Bn,progressPercent:()=>Dn});var Cn=.5,Nn=3,On=1.35,Ln=280,$n=\"500px 0px\",Hn=\"-48% 0px -48% 0px\";function Un(e){let n=Math.round((Number(e)||0)*10)/10;return Math.max(.5,Math.min(3,n))}function Ue(e,n){return Math.max(1,Math.min(n||1,Number(e)||1))}function Dn(e,n){return n?Math.round(Ue(e,n)/n*100):0}function Wn(e,n){return{prevDisabled:e<=1,nextDisabled:!!n&&e>=n}}var Vn=28;function zn(e){var a;let n=(a=e.zoom)!=null?a:1,t=Math.max(280,e.containerWidth||900)*n;if(!e.onePage)return t;let r=e.aspect||1.35,s=((e.containerHeight||600)-Vn-(e.overlap||0)-(e.reserve||0))/r;return s>120?Math.min(t,s):t}function Fn(e){let n=e.length/4;if(!n)return\"rgb(0,0,0)\";let t=0,r=0,o=0;for(let s=0;s<e.length;s+=4)t+=e[s],r+=e[s+1],o+=e[s+2];return`rgb(${Math.round(t/n)},${Math.round(r/n)},${Math.round(o/n)})`}function Bn(e,n){return/[.](png|jpe?g|webp|gif|avif)($|[?])/i.test(String(e||n||\"\"))}var V={};M(V,{presentationTransition:()=>jn,switchDocUrl:()=>Gn});function jn(e,n={}){if(!e)return[{kind:\"nothing\"}];if(e.active===!1)return[{kind:\"ended\"}];let t=le(e.content);if(t)return[{kind:\"show-map\",content:t}];let r=[{kind:\"leave-map\"}];return e.file_url&&e.file_url!==n.docUrl?(r.push({kind:\"switch-doc\",url:e.file_url,name:e.file_name||void 0,title:e.doc_title||void 0,updatedAt:e.updated_at||void 0}),r):(e.current_page&&r.push({kind:\"show-page\",page:e.current_page}),r)}function Gn(e,n){let t=encodeURIComponent(n||String(Date.now()));return`${e}${e.includes(\"?\")?\"&\":\"?\"}v=${t}`}return Ge(Kn);})();", SOURCE_HASH: "24b1b68db3671718" };
package/server/cache.js CHANGED
@@ -24,6 +24,14 @@
24
24
  // 3. UN ÉCHEC NE SE MÉMORISE PAS. Servir une erreur pendant toute la fenêtre transformerait un
25
25
  // hoquet en panne visible, et retarderait le rétablissement. On oublie la promesse rompue.
26
26
  //
27
+ // 4. ⚠️ LE PLAFOND DES RÉSULTATS NE BORNE PAS LES DEMANDES EN VOL — et c'est par là que ça cédait.
28
+ // Le piège 2 était traité pour les valeurs RETENUES, mais une entrée en vol n'est jamais évincée
29
+ // (piège 1 : l'évincer casserait le regroupement). Tant que la base ne répond pas, RIEN ne
30
+ // bornait donc l'admission : 10 000 clés distinctes pendantes donnaient 10 000 entrées ET 10 000
31
+ // producteurs — donc 10 000 requêtes, sockets et promesses ouvertes, avec `max: 100`. Mesuré, pas
32
+ // supposé. Il faut un plafond SÉPARÉ sur les demandes en vol, et un REFUS quand il est atteint :
33
+ // borner l'admission est la seule chose qui borne une base lente. (Relevé par un audit externe.)
34
+ //
27
35
  // ⚠️ CE QUE CE CACHE NE FAIT PAS : il vit dans la mémoire du PROCESSUS. En serverless, plusieurs
28
36
  // instances servent en parallèle et démarrent à froid — l'effondrement est donc « une lecture par
29
37
  // fenêtre ET PAR INSTANCE », pas « une lecture ». C'est la même limite que celle d'un compteur de
@@ -35,6 +43,21 @@
35
43
  *
36
44
  * @param {{ ttlMs: number, max?: number, now?: () => number }} options
37
45
  */
46
+ /**
47
+ * ⚠️ UNE ERREUR TYPÉE, PAS UN `null` NI UN THROW ANONYME. La route doit pouvoir distinguer « la base
48
+ * a échoué » (500) de « nous refusons d'admettre une demande de plus » (503, réessayable) : ce sont
49
+ * deux situations opposées pour l'appelant, et les confondre lui ferait abandonner une requête qui
50
+ * n'attendait qu'une seconde.
51
+ */
52
+ const CODE_SATURATION = "cache-sature";
53
+ function erreurSaturation(plafond) {
54
+ const e = new Error(`cache saturé : ${plafond} demandes déjà en vol`);
55
+ e.code = CODE_SATURATION;
56
+ e.statusCode = 503;
57
+ e.retryAfter = 1;
58
+ return e;
59
+ }
60
+
38
61
  function creerCache(options) {
39
62
  const ttl = Math.max(0, Number(options && options.ttlMs) || 0);
40
63
  // ⚠️ DEUX PLAFONDS, ET LE SECOND EST CELUI QUI COMPTE. Un plafond en NOMBRE d'entrées ne borne rien
@@ -44,15 +67,25 @@ function creerCache(options) {
44
67
  // valeurs mises en cache ici sont des chaînes JSON. (Relevé par un audit externe.)
45
68
  const max = Math.max(1, Number(options && options.max) || 100);
46
69
  const maxOctets = Math.max(1, Number(options && options.maxOctets) || 8 * 1024 * 1024);
70
+ // ⚠️ LE TROISIÈME PLAFOND, ET C'EST LUI QUI TIENT SOUS UNE BASE LENTE. Les deux autres ne bornent
71
+ // que ce qui est RÉSOLU ; celui-ci borne ce qui est ADMIS. Une valeur volontairement basse devant
72
+ // `max` : une entrée en vol coûte une requête, une socket et une promesse, bien plus cher qu'un
73
+ // résultat en mémoire.
74
+ const maxEnVol = Math.max(1, Number(options && options.maxEnVol) || 128);
47
75
  const now = (options && options.now) || (() => Date.now());
48
- /** @type {Map<string, { echeance: number, promesse: Promise<unknown>, poids: number, enVol: boolean }>} */
76
+ /** @type {Map<string, { echeance: number, promesse: Promise<unknown>, poids: number, enVol: boolean, rendrePlace?: () => void }>} */
49
77
  const entrees = new Map();
50
78
  let poidsTotal = 0;
79
+ let nEnVol = 0; // tenu à jour à chaque transition — compter la Map à la demande serait O(n)
51
80
 
52
81
  const oublier = (k) => {
53
82
  const e = entrees.get(k);
54
83
  if (!e) return;
55
84
  poidsTotal -= e.poids;
85
+ // Une entrée en vol qu'on retire (promesse rompue) libère sa place d'admission — sans quoi le
86
+ // plafond se remplirait définitivement au premier incident de base. Le rendu passe par le verrou
87
+ // de la promesse : c'est lui qui garantit qu'une place n'est rendue qu'une fois.
88
+ if (e.enVol && typeof e.rendrePlace === "function") e.rendrePlace();
56
89
  entrees.delete(k);
57
90
  };
58
91
 
@@ -82,25 +115,41 @@ function creerCache(options) {
82
115
  // AUTRE la pousse dehors : le cache gardait indéfiniment ce qu'il ne servait plus.
83
116
  purger(t);
84
117
  const vue = entrees.get(k);
85
- // Une entrée en vol se partage toujours : c'est tout l'objet du regroupement.
118
+ // ⚠️ UNE CLÉ DÉJÀ EN VOL SE PARTAGE TOUJOURS, MÊME AU PLAFOND. C'est l'ordre de ces deux tests
119
+ // qui décide : refuser d'abord ferait échouer des appelants que le regroupement pouvait servir
120
+ // gratuitement — la saturation punirait alors la rafale légitime, exactement ce que ce cache
121
+ // existe pour absorber. On ne refuse que ce qui coûterait une requête DE PLUS.
86
122
  if (vue && (vue.enVol || vue.echeance > t)) return vue.promesse;
87
123
 
124
+ if (nEnVol >= maxEnVol) throw erreurSaturation(maxEnVol);
125
+
88
126
  const promesse = Promise.resolve().then(produire);
127
+ // ⚠️ UN DÉCOMPTE IDEMPOTENT PAR PROMESSE. Deux chemins libèrent la place (résolution, et
128
+ // `oublier` sur rejet) : sans ce verrou, une place pouvait être rendue DEUX fois, `nEnVol`
129
+ // passait sous zéro et le plafond cessait silencieusement de refuser — un plafond qui ne peut
130
+ // plus être atteint ne se distingue pas d'un plafond absent.
131
+ let placeRendue = false;
132
+ const rendrePlace = () => { if (!placeRendue) { placeRendue = true; nEnVol -= 1; } };
133
+ nEnVol += 1;
89
134
  // ⚠️ L'ÉCHÉANCE PART DE LA RÉSOLUTION, PAS DE LA DEMANDE. Posée à la demande, elle expirait
90
135
  // AVANT que la production ne réponde dès que celle-ci dépassait le TTL — et le regroupement ne
91
136
  // servait alors plus à rien pour exactement les producteurs lents, les seuls qu'il valait la
92
137
  // peine de mutualiser. Tant qu'elle est en vol, l'entrée ne périme pas.
93
- entrees.set(k, { echeance: 0, promesse, poids: 0, enVol: true });
138
+ entrees.set(k, { echeance: 0, promesse, poids: 0, enVol: true, rendrePlace });
94
139
 
95
140
  promesse.then(
96
141
  (valeur) => {
97
142
  const courante = entrees.get(k);
143
+ rendrePlace();
98
144
  if (!courante || courante.promesse !== promesse) return;
99
145
  courante.enVol = false;
100
146
  courante.echeance = now() + ttl;
101
- // Le poids réel : ces valeurs sont des chaînes JSON. Une valeur d'une autre forme compte
102
- // pour un poids nominal — mieux vaut sous-estimer que prétendre mesurer ce qu'on ignore.
103
- courante.poids = typeof valeur === "string" ? valeur.length : 1;
147
+ // ⚠️ DES OCTETS, PAS DES CARACTÈRES. `.length` compte des unités UTF-16 : « 界 » y vaut 1
148
+ // et pèse 3 octets en mémoire comme sur le fil. Un contenu non latin sous-estimait donc le
149
+ // poids d'un facteur 3 — un plafond « 8 Mo » en laissait passer 24. La valeur d'une autre
150
+ // forme compte pour un poids nominal : mieux vaut sous-estimer que prétendre mesurer ce
151
+ // qu'on ignore. (Relevé par un audit externe.)
152
+ courante.poids = typeof valeur === "string" ? Buffer.byteLength(valeur, "utf8") : 1;
104
153
  poidsTotal += courante.poids;
105
154
  borner();
106
155
  },
@@ -109,6 +158,7 @@ function creerCache(options) {
109
158
  // resterait servi pendant toute la fenêtre, et le rétablissement attendrait pour rien.
110
159
  const courante = entrees.get(k);
111
160
  if (courante && courante.promesse === promesse) oublier(k);
161
+ rendrePlace(); // idempotent : `oublier` a pu le faire, ou l'entrée avoir été remplacée
112
162
  },
113
163
  );
114
164
  return promesse;
@@ -116,9 +166,11 @@ function creerCache(options) {
116
166
 
117
167
  /** Pour les tests et l'exploitation : ce que la table contient réellement. */
118
168
  taille: () => entrees.size,
119
- /** Poids cumulé des résultats retenus, en caractères. */
169
+ /** Poids cumulé des résultats retenus, en OCTETS UTF-8. */
120
170
  poids: () => poidsTotal,
171
+ /** Demandes actuellement en vol — ce que le plafond d'admission borne. */
172
+ enVol: () => nEnVol,
121
173
  };
122
174
  }
123
175
 
124
- module.exports = { creerCache };
176
+ module.exports = { creerCache, CODE_SATURATION };
package/server/handler.js CHANGED
@@ -7,8 +7,8 @@ const crypto = require("crypto");
7
7
  const { Readable } = require("node:stream");
8
8
  const { pipeline } = require("node:stream/promises");
9
9
  const { getShareBySlug } = require("./shares");
10
- const { PRESENT_QUOTA_PER_HOUR, PRESENT_CACHE_MS } = require("./shared.generated.js");
11
- const { creerCache } = require("./cache.js");
10
+ const { PRESENT_QUOTA_PER_HOUR, PRESENT_CACHE_MS, estSlug } = require("./shared.generated.js");
11
+ const { creerCache, CODE_SATURATION } = require("./cache.js");
12
12
 
13
13
  // ⚠️ UN SEUL CACHE POUR TOUT LE PROCESSUS, ET C'EST LE POINT. Le créer par requête reviendrait à
14
14
  // n'en avoir aucun : chaque appelant repartirait d'une table vide, et l'effondrement qu'on
@@ -797,10 +797,31 @@ async function handler(req, res) {
797
797
  for (const lecture of LECTURES_PARTAGEES) {
798
798
  if (String(q[lecture.param] || "") !== "1") continue;
799
799
  const slugLu = String(q.present);
800
- const corps = await cacheLecture.lire(`${lecture.param}:${slugLu}${lecture.suffixeCle ? lecture.suffixeCle() : ""}`, async () => {
801
- const p = await getPresentation(slugLu);
802
- return p ? JSON.stringify(await lecture.produire(p, slugLu)) : null;
803
- });
800
+ // ⚠️ LE CONTRAT S'APPLIQUE AVANT LE CACHE ET AVANT LA BASE. Le slug vient de l'URL : non
801
+ // validé, il entrait tel quel dans une CLÉ DE CACHE et dans une requête base. Un lecteur
802
+ // faisait donc varier les clés à volonté — c'est ce qui rendait l'admission du cache
803
+ // actionnable de l'extérieur. Le motif vit dans `shared.ts`, en un seul exemplaire.
804
+ if (!estSlug(slugLu)) return sendRefusal(res, "ended", embed);
805
+ let corps;
806
+ try {
807
+ corps = await cacheLecture.lire(`${lecture.param}:${slugLu}${lecture.suffixeCle ? lecture.suffixeCle() : ""}`, async () => {
808
+ const p = await getPresentation(slugLu);
809
+ return p ? JSON.stringify(await lecture.produire(p, slugLu)) : null;
810
+ });
811
+ } catch (e) {
812
+ // ⚠️ « NOUS REFUSONS D'ADMETTRE UNE DEMANDE DE PLUS » N'EST PAS « LA BASE A ÉCHOUÉ ». Un
813
+ // 503 réessayable dit à l'appelant d'attendre une seconde ; un 500 lui dit d'abandonner.
814
+ // Les confondre ferait renoncer une requête que le prochain battement aurait servie.
815
+ if (e && e.code === CODE_SATURATION) {
816
+ res.statusCode = 503;
817
+ res.setHeader("Retry-After", String(e.retryAfter || 1));
818
+ res.setHeader("Cache-Control", "no-store");
819
+ res.setHeader("Content-Type", "application/json");
820
+ res.end(JSON.stringify({ ok: false, error: "busy" }));
821
+ return;
822
+ }
823
+ throw e;
824
+ }
804
825
  if (!corps) return sendRefusal(res, "ended", embed);
805
826
  res.statusCode = 200; res.setHeader("Content-Type", "application/json"); res.setHeader("Cache-Control", "no-store");
806
827
  res.end(corps);
@@ -94,6 +94,17 @@ function presentHtml(pres, nonce, logoUrl, supaUrl, supaKey) {
94
94
  (function(){
95
95
  var CFG=${cfg};
96
96
  var PDF=null, total=0, cur=CFG.page||1, ready=false;
97
+ // ⚠️ DEUX GÉNÉRATIONS, PARCE QU'IL Y A DEUX COURSES DISTINCTES (P1 audit externe).
98
+ //
99
+ // docGen : le présentateur change de document pendant qu'un chargement est en cours. Le
100
+ // callback de l'ANCIEN document arrivait après le nouveau et posait ses pages par-dessus.
101
+ // renderGen : deux getPage() en vol (changement de page rapide, redimensionnement) peuvent
102
+ // se résoudre DANS LE DÉSORDRE — pdf.js ne garantit aucun ordre entre deux pages. Le plus lent
103
+ // gagnait, et l'audience restait sur une page que personne n'avait demandée.
104
+ //
105
+ // Une génération par course : les confondre ferait qu'un simple changement de page invaliderait
106
+ // le document, ou l'inverse.
107
+ var docGen=0, renderGen=0, tacheRendu=null, chargement=null;
97
108
  var stage=document.getElementById('stage'), pageEl=document.getElementById('page');
98
109
  function hideLoader(){ var l=document.getElementById('load'); if(l){ l.classList.add('hide'); setTimeout(function(){ if(l.parentNode)l.parentNode.removeChild(l); },450);} }
99
110
  function ended(){ document.getElementById('ended').style.display='flex';
@@ -115,7 +126,14 @@ function presentHtml(pres, nonce, logoUrl, supaUrl, supaKey) {
115
126
  pgi.innerHTML=''; pgi.appendChild(el);
116
127
  return;
117
128
  }
129
+ // ⚠️ ON ANNULE LE RENDU PRÉCÉDENT AVANT D'EN LANCER UN AUTRE. Le RenderTask était jeté :
130
+ // deux rendus peignaient alors en parallèle, et le plus lent finissait par l'emporter.
131
+ if(tacheRendu){ try{ tacheRendu.cancel(); }catch(e){} tacheRendu=null; }
132
+ var monDoc=docGen, monRendu=++renderGen;
118
133
  PDF.getPage(n).then(function(page){
134
+ // Le document a changé, ou une page plus récente a été demandée pendant que celle-ci
135
+ // arrivait : ce résultat n'est plus celui qu'on attend. On ne le pose pas.
136
+ if(monDoc!==docGen || monRendu!==renderGen) return;
119
137
  var availW=Math.max(120, stage.clientWidth-40), availH=Math.max(120, stage.clientHeight-40);
120
138
  var v1=page.getViewport({scale:1});
121
139
  var scale=Math.min(availW/v1.width, availH/v1.height); // fit entier → une page à l'écran
@@ -123,9 +141,18 @@ function presentHtml(pres, nonce, logoUrl, supaUrl, supaKey) {
123
141
  var cv=document.createElement('canvas'); cv.width=Math.floor(vp.width*dpr); cv.height=Math.floor(vp.height*dpr);
124
142
  cv.setAttribute('role','img'); cv.setAttribute('aria-label','Page '+n+(total>1?' sur '+total:''));
125
143
  cv.style.width=vp.width+'px'; cv.style.height=vp.height+'px';
126
- page.render({canvasContext:cv.getContext('2d'),viewport:vp,transform:dpr!==1?[dpr,0,0,dpr,0,0]:null});
127
- pageEl.innerHTML=''; pageEl.appendChild(cv);
128
- });
144
+ var t=page.render({canvasContext:cv.getContext('2d'),viewport:vp,transform:dpr!==1?[dpr,0,0,dpr,0,0]:null});
145
+ tacheRendu=t;
146
+ // ⚠️ ON NE PUBLIE QU'APRÈS LE RENDU. L'ancien code posait le canvas AVANT que pdf.js n'ait
147
+ // peint : la page apparaissait blanche puis se remplissait, et un rendu périmé pouvait
148
+ // encore écraser le bon. On revérifie la génération APRÈS l'attente — c'est là qu'elle a pu
149
+ // changer, pas avant.
150
+ return t.promise.then(function(){
151
+ if(monDoc!==docGen || monRendu!==renderGen) return;
152
+ if(tacheRendu===t) tacheRendu=null;
153
+ pageEl.innerHTML=''; pageEl.appendChild(cv);
154
+ });
155
+ }).catch(function(){ /* rendu annulé ou page illisible : le rendu courant fait foi */ });
129
156
  }
130
157
  function boot(){
131
158
  import(CFG.pdfjs).then(function(m){
@@ -170,35 +197,67 @@ function presentHtml(pres, nonce, logoUrl, supaUrl, supaKey) {
170
197
  // réponse n est jamais « celui que le banc sait voir » : on garde le champ qui fait foi, puis on
171
198
  // rend le banc capable de le distinguer — un cas où l URL ment et où seul le nom dit vrai.
172
199
  function estImage(){ return Player.viewer.isImageDocument(CFG.fileName, CFG.docUrl); }
173
- function chargerImage(){
200
+ function chargerImage(monDoc){
174
201
  var im=new Image();
175
202
  im.alt='Document présenté';
176
203
  im.onload=function(){
204
+ // Le présentateur a pu rebasculer pendant le téléchargement : cette image n'est plus celle
205
+ // du document courant.
206
+ if(monDoc!==docGen) return;
177
207
  total=1; ready=true; PDF={image:im};
178
208
  var tot=document.getElementById('tot'); if(tot)tot.textContent=1;
179
209
  show(1); hideLoader();
180
210
  if(!CFG.active) ended();
181
211
  };
182
- im.onerror=function(){ var l=document.getElementById('load'); if(l)l.querySelector('.lsub').textContent="Document indisponible."; };
212
+ im.onerror=function(){ if(monDoc!==docGen) return; var l=document.getElementById('load'); if(l)l.querySelector('.lsub').textContent="Document indisponible."; };
183
213
  im.src=CFG.fileUrl;
184
214
  }
215
+ // ⚠️ ON LIBÈRE L'ANCIEN DOCUMENT. Un PDFDocumentProxy retient ses pages, ses canvas et son
216
+ // worker : enchaîner les bascules sans détruire faisait croître la mémoire sans borne, et c'est
217
+ // le mode de panne d'une présentation longue — celle qui dure, pas celle qu'on teste.
218
+ function libererDocument(){
219
+ if(tacheRendu){ try{ tacheRendu.cancel(); }catch(e){} tacheRendu=null; }
220
+ if(chargement){ try{ chargement.destroy(); }catch(e){} chargement=null; }
221
+ if(PDF && !PDF.image && typeof PDF.destroy==='function'){ try{ PDF.destroy(); }catch(e){} }
222
+ PDF=null;
223
+ }
185
224
  function load(){
186
- if(estImage()){ chargerImage(); return; }
187
- pdfjsLib.getDocument({url:CFG.fileUrl,isEvalSupported:false}).promise.then(function(pdf){
225
+ var monDoc=docGen;
226
+ if(estImage()){ chargerImage(monDoc); return; }
227
+ var t=pdfjsLib.getDocument({url:CFG.fileUrl,isEvalSupported:false});
228
+ chargement=t;
229
+ t.promise.then(function(pdf){
230
+ // Une bascule est survenue pendant le chargement : ce document n'est plus attendu, et le
231
+ // garder fuirait. On le détruit plutôt que de simplement l'ignorer.
232
+ if(monDoc!==docGen){ try{ pdf.destroy(); }catch(e){} return; }
188
233
  PDF=pdf; total=pdf.numPages; ready=true;
189
234
  var tot=document.getElementById('tot'); if(tot)tot.textContent=total;
190
235
  show(cur); hideLoader();
191
236
  if(!CFG.active) ended();
192
- }).catch(function(){ var l=document.getElementById('load'); if(l)l.querySelector('.lsub').textContent="Document indisponible."; });
237
+ }).catch(function(){ if(monDoc!==docGen) return; var l=document.getElementById('load'); if(l)l.querySelector('.lsub').textContent="Document indisponible."; });
193
238
  }
194
239
  // Le présentateur a changé de document (même session) → recharger le PDF servi par le proxy (URL identique →
195
240
  // cache-buster obligatoire), remettre à la page 1, mettre à jour le titre.
241
+ // ⚠️ TOUT CE QUI DÉCRIT LE DOCUMENT CHANGE ENSEMBLE, fileName COMPRIS — c'était le P1.
242
+ //
243
+ // fileName n'était PAS mis à jour, alors que c'est lui qui fait foi pour décider image ou PDF
244
+ // (estImage, et le commentaire plus bas dit pourquoi). Après une bascule, le nom restait celui
245
+ // du document précédent : un PDF→PNG envoyait donc le nouveau PNG à pdf.js, et un PNG→PDF
246
+ // chargeait le nouveau PDF avec new Image(). Dans les deux cas : « Document indisponible ».
247
+ //
248
+ // Une mise à jour partielle d'un état qui se lit comme un TOUT est la forme du défaut : il n'y a
249
+ // pas de moment où CFG doit décrire un document par son URL et un autre par son nom.
196
250
  function switchDoc(row){
251
+ docGen++; // invalide tout callback du document précédent
252
+ libererDocument();
197
253
  var t=document.getElementById('ptitle'); if(t) t.textContent=row.doc_title||row.file_name||'Document';
198
254
  var pg=document.getElementById('page'); if(pg) pg.innerHTML='';
199
255
  cur=1; total=0; ready=false;
200
256
  var tot=document.getElementById('tot'); if(tot)tot.textContent='—';
201
257
  var cu=document.getElementById('cur'); if(cu)cu.textContent='1';
258
+ if(row.file_url) CFG.docUrl=row.file_url;
259
+ CFG.fileName=row.file_name||'';
260
+ CFG.title=row.doc_title||row.file_name||'Document';
202
261
  CFG.fileUrl='/api/doc?present='+encodeURIComponent(CFG.slug)+'&file=1&v='+encodeURIComponent(row.updated_at||String(row.current_page||1));
203
262
  load();
204
263
  }
@@ -229,7 +288,9 @@ function presentHtml(pres, nonce, logoUrl, supaUrl, supaKey) {
229
288
  if(a.kind==='ended'){ ended(); return; }
230
289
  if(a.kind==='show-map'){ if(window.Map3DD){ if(a.content.kind==='streetview') Map3DD.enterSV(a.content,false); else Map3DD.enter(a.content,false); } return; }
231
290
  if(a.kind==='leave-map'){ if(window.Map3DD) Map3DD.exit(); }
232
- if(a.kind==='switch-doc'){ CFG.docUrl=a.url; switchDoc(row); return; }
291
+ // switchDoc pose LUI-MÊME docUrl/fileName/titre : les poser ici, en partie, était
292
+ // exactement ce qui laissait fileName en arrière.
293
+ if(a.kind==='switch-doc'){ switchDoc(row); return; }
233
294
  if(a.kind==='show-page'){ show(a.page); }
234
295
  }
235
296
  }
@@ -44,9 +44,11 @@ __export(shared_exports, {
44
44
  SESSION_INTERVAL_MS: () => SESSION_INTERVAL_MS,
45
45
  SESSION_QUOTA_PER_HOUR: () => SESSION_QUOTA_PER_HOUR,
46
46
  SESSION_WRITES_PER_HOUR: () => SESSION_WRITES_PER_HOUR,
47
+ SLUG_RE: () => SLUG_RE,
47
48
  VIEW_EVENTS_PER_READER_HOUR: () => VIEW_EVENTS_PER_READER_HOUR,
48
49
  VIEW_QUOTA_PER_HOUR: () => VIEW_QUOTA_PER_HOUR,
49
50
  cycleMapType: () => cycleMapType,
51
+ estSlug: () => estSlug,
50
52
  initialMapContent: () => initialMapContent,
51
53
  mapTypeLabel: () => mapTypeLabel,
52
54
  sanitizeContent: () => sanitizeContent
@@ -121,6 +123,12 @@ var PRESENT_READ_COALESCE_MS = 400;
121
123
  var PRESENT_CACHE_MS = PRESENT_READ_COALESCE_MS;
122
124
  var PRESENT_QUOTA_MARGIN = 4;
123
125
  var PRESENT_QUOTA_PER_HOUR = (PRESENT_READS_PER_HOUR + PRESENT_READ_BURST) * READERS_PER_EGRESS * PRESENT_QUOTA_MARGIN;
126
+
127
+ // src/shared.ts
128
+ var SLUG_RE = /^[A-Za-z0-9_-]{1,64}$/;
129
+ function estSlug(v) {
130
+ return typeof v === "string" && SLUG_RE.test(v);
131
+ }
124
132
  // Annotate the CommonJS export names for ESM import in node:
125
133
  0 && (module.exports = {
126
134
  DEFAULT_CENTER,
@@ -142,9 +150,11 @@ var PRESENT_QUOTA_PER_HOUR = (PRESENT_READS_PER_HOUR + PRESENT_READ_BURST) * REA
142
150
  SESSION_INTERVAL_MS,
143
151
  SESSION_QUOTA_PER_HOUR,
144
152
  SESSION_WRITES_PER_HOUR,
153
+ SLUG_RE,
145
154
  VIEW_EVENTS_PER_READER_HOUR,
146
155
  VIEW_QUOTA_PER_HOUR,
147
156
  cycleMapType,
157
+ estSlug,
148
158
  initialMapContent,
149
159
  mapTypeLabel,
150
160
  sanitizeContent