@fanfare-io/fanfare-sdk-core 0.9.0 → 0.10.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.
Files changed (65) hide show
  1. package/dist/admission-proof.d.ts +2 -0
  2. package/dist/admission-proof.js +1 -0
  3. package/dist/appointments/appointment.driver.js +1 -1
  4. package/dist/appointments/appointment.module.d.ts +2 -2
  5. package/dist/appointments/appointment.module.js +1 -1
  6. package/dist/appointments/appointment.sequence.d.ts +1 -5
  7. package/dist/appointments/types.d.ts +1 -1
  8. package/dist/auctions/auction.driver.js +1 -1
  9. package/dist/auctions/auction.module.d.ts +9 -2
  10. package/dist/auctions/auction.module.js +1 -1
  11. package/dist/auctions/auction.sequence.d.ts +4 -14
  12. package/dist/core/client.js +1 -1
  13. package/dist/core/error-display.js +1 -1
  14. package/dist/core/error-disposition.js +1 -1
  15. package/dist/core/errors.d.ts +10 -0
  16. package/dist/core/errors.js +1 -1
  17. package/dist/core/http.d.ts +1 -0
  18. package/dist/core/http.js +1 -1
  19. package/dist/core/poll-scheduler.d.ts +21 -0
  20. package/dist/core/poll-scheduler.js +1 -0
  21. package/dist/core/utils.d.ts +2 -0
  22. package/dist/core/utils.js +1 -1
  23. package/dist/draws/draw.driver.js +1 -1
  24. package/dist/draws/draw.module.d.ts +3 -0
  25. package/dist/draws/draw.module.js +1 -1
  26. package/dist/draws/draw.sequence.d.ts +2 -6
  27. package/dist/experiences/distribution-monitor.types.d.ts +1 -1
  28. package/dist/experiences/journey-view.js +1 -1
  29. package/dist/experiences/journey.d.ts +8 -0
  30. package/dist/experiences/journey.js +1 -1
  31. package/dist/experiences/journey.machine.d.ts +3 -0
  32. package/dist/experiences/journey.machine.js +1 -1
  33. package/dist/experiences/journey.types.d.ts +26 -16
  34. package/dist/experiences/sequence-driver.d.ts +7 -11
  35. package/dist/experiences/types.d.ts +1 -1
  36. package/dist/queues/queue.driver.js +1 -1
  37. package/dist/queues/queue.module.d.ts +5 -2
  38. package/dist/queues/queue.module.js +1 -1
  39. package/dist/queues/queue.sequence.d.ts +2 -6
  40. package/dist/state/capability-token-registry.d.ts +15 -3
  41. package/dist/state/capability-token-registry.js +1 -1
  42. package/dist/state/events.d.ts +2 -2
  43. package/dist/state/store.d.ts +4 -1
  44. package/dist/state/store.js +1 -1
  45. package/dist/sync/cross-tab-coordinator.js +1 -1
  46. package/dist/sync/index.d.ts +1 -1
  47. package/dist/sync/protocol-transport.d.ts +3 -2
  48. package/dist/sync/protocol-transport.js +1 -1
  49. package/dist/sync/protocol.d.ts +30 -1
  50. package/dist/sync/protocol.js +1 -1
  51. package/dist/test-utils/harness-scenarios.js +1 -1
  52. package/dist/test-utils/mock-journey.d.ts +2 -2
  53. package/dist/test-utils/mock-journey.js +1 -1
  54. package/dist/test-utils/verification-scenarios.d.ts +1 -1
  55. package/dist/test-utils/verification-scenarios.js +1 -1
  56. package/dist/timed-releases/timed-release.driver.js +1 -1
  57. package/dist/timed-releases/timed-release.sequence.d.ts +2 -6
  58. package/dist/types/distribution-type.d.ts +10 -22
  59. package/dist/version.d.ts +1 -1
  60. package/dist/version.js +1 -1
  61. package/package.json +7 -7
  62. package/dist/handoff/handoff.module.d.ts +0 -184
  63. package/dist/handoff/handoff.module.js +0 -1
  64. package/dist/handoff/index.d.ts +0 -6
  65. package/dist/handoff/index.js +0 -1
@@ -0,0 +1,2 @@
1
+ export { buildAdmissionProofHeaders, canonicalizeAdmissionProofUrl } from './security/admission-proof';
2
+ export type { AdmissionProofHeaders } from './security/admission-proof';
@@ -0,0 +1 @@
1
+ import{buildAdmissionProofHeaders as o,canonicalizeAdmissionProofUrl as r}from"./security/admission-proof.js";export{o as buildAdmissionProofHeaders,r as canonicalizeAdmissionProofUrl};
@@ -1 +1 @@
1
- import{createError as t}from"../core/errors.js";function i(t){return new o(t)}function e(i){const{distribution:e,consumer:n,waitlist:o,outcome:a}=i;if(!e)return{phase:"unavailable"};const r=function(i){if("appointment"!==i.type)throw t.validationError("Appointment driver received a non-appointment distribution");return i}(e),p=n&&"appointment"===n.mechanism?function(t){const{mechanism:i,...e}=t;return e}(n):function(t){return{status:"not_booked",distributionId:t.id}}(e),d=a??function(t){return"completed"===t.status?{type:"completed"}:"cancelled"===t.status?{type:"cancelled"}:"no_show"===t.status?{type:"no_show"}:void 0}(p);if(d)return s(r,p,d);if(function(t){return"booked"===t.status||"checked_in"===t.status}(p))return{phase:"participating",mechanism:"appointment",distribution:r,consumer:p};if("scheduled"===r.lifecycle){const t=o&&o.waitlistId===r.waitlistId?o:r.waitlistId?{waitlistId:r.waitlistId,status:"not_waitlisted"}:void 0;return{phase:"scheduled",mechanism:"appointment",distribution:r,...t?{waitlist:t}:{}}}return"open"===r.lifecycle?{phase:"enterable",mechanism:"appointment",distribution:r,consumer:p}:s(r,p,{type:"closed"})}function n(t,i){return t&&"appointment"===t.mechanism&&i?"cancelled"===i.type?{...t,status:"cancelled"}:"no_show"===i.type?{...t,status:"no_show"}:"completed"===i.type?{...t,status:"completed"}:{...t,status:"not_booked"}:t}class o{constructor(t){this.deps=t,this.mechanism="appointment"}project(t){return e(t)}buildView(i,e){if("unavailable"===i.phase)return{phase:"unavailable",reason:i.reason};const n=function(i){if(!("mechanism"in i)||"appointment"!==i.mechanism)throw t.validationError("Appointment driver received a non-appointment sequence");return i}(i);switch(n.phase){case"scheduled":return{phase:"scheduled",mechanism:"appointment",distribution:n.distribution,startsAt:n.distribution.startsAt??n.distribution.opensAt,waitlist:n.waitlist?{status:n.waitlist.status,join:()=>e.joinWaitlist(),leave:()=>e.exitWaitlist()}:void 0};case"enterable":return{phase:"enterable",mechanism:"appointment",distribution:n.distribution,consumer:n.consumer,book:(t,i)=>this.book(t,i)};case"participating":return{phase:"participating",mechanism:"appointment",consumer:n.consumer,display$:e.displayAtom,cancel:t=>this.cancel(t),reschedule:(t,i)=>this.reschedule(t,i)};case"ended":return{phase:"ended",mechanism:"appointment",consumer:n.consumer,outcome:n.outcome}}}startMonitoring(t){this.activeMonitorId=t.participation.id,this.deps.appointments.startMonitoring(t.participation.id,{displayAtom:t.displayAtom},t.onUpdate)}stopMonitoring(){this.activeMonitorId&&(this.deps.appointments.stopMonitoring(this.activeMonitorId),this.activeMonitorId=void 0)}async book(t,i){await this.deps.runSerializedOperation(async()=>{const n=this.currentAppointmentSequence("enterable","No open appointment to book").distribution,o=await this.deps.appointments.book(n.id,t,i);this.deps.appointments.bumpMonitorGeneration(n.id),this.writeBookingDisplay(n.id,o),this.deps.commit(e({distribution:n,consumer:{mechanism:"appointment",status:"booked",id:n.id,distributionId:n.id,slotId:o.slotId,locationId:o.locationId}})),this.deps.emitEvent("sequence_change","success","user","Booked appointment slot",{appointmentId:n.id,slotId:t,locationId:i})})}async cancel(i){await this.deps.runSerializedOperation(async()=>{const e=this.currentAppointmentSequence("participating"),n=e.consumer,o=this.deps.getDisplayState(),s=o&&"appointment"===o.type?o.booking:void 0;if(!s)throw t.validationError("No booking details available to cancel");const a=n.distributionId??n.id??e.distribution.id;await this.deps.appointments.cancel(a,s.slotId,s.locationId??"",i),this.deps.stopRuntimeMonitoring(!0),this.deps.applyMonitorUpdate({type:"ended",outcome:{type:"cancelled",reason:i}})})}async reschedule(t,i){await this.deps.runSerializedOperation(async()=>{const e=this.currentAppointmentSequence("participating"),n=e.consumer,o=n.distributionId??n.id??e.distribution.id,s=await this.deps.appointments.reschedule(o,t,i);this.deps.appointments.bumpMonitorGeneration(o),this.writeBookingDisplay(o,s),this.deps.emitEvent("sequence_change","info","user","Rescheduled appointment",{appointmentId:o,newSlotId:t,newLocationId:i})})}writeBookingDisplay(t,i){const e=this.deps.ensureDisplayAtom({id:t,type:"appointment"}),n=Math.max(0,Date.parse(i.startTime)-Date.now());e.set({type:"appointment",booking:{slotId:i.slotId,locationId:i.locationId,locationName:i.locationName,joinUrl:i.joinUrl,startTime:i.startTime,endTime:i.endTime,confirmationCode:i.confirmationCode,consumerStatus:i.consumerStatus},msUntilStart:n})}currentAppointmentSequence(i,e="Not participating in an appointment"){const n=this.deps.getCurrentSequence();if(!n||n.phase!==i||!("mechanism"in n)||"appointment"!==n.mechanism)throw t.validationError(e);return n}}function s(t,i,e){return{phase:"ended",mechanism:"appointment",distribution:t,consumer:i,outcome:{...e,distributionType:e.distributionType??t.type,distributionId:e.distributionId??t.id,at:e.at??/* @__PURE__ */(new Date).toISOString()}}}export{i as createAppointmentDriver,e as projectAppointmentSequence,n as withAppointmentTerminalConsumer};
1
+ import{createError as t}from"../core/errors.js";function e(t){return new o(t)}function i(e){const{distribution:i,consumer:n,outcome:o}=e;if(!i)return{phase:"unavailable"};const a=function(e){if("appointment"!==e.type)throw t.validationError("Appointment driver received a non-appointment distribution");return e}(i),r=n&&"appointment"===n.mechanism?function(t){const{mechanism:e,...i}=t;return i}(n):function(t){return{status:"not_booked",distributionId:t.id}}(i),p=o??function(t){return"completed"===t.status?{type:"completed"}:"cancelled"===t.status?{type:"cancelled"}:"no_show"===t.status?{type:"no_show"}:void 0}(r);return p?s(a,r,p):function(t){return"booked"===t.status||"checked_in"===t.status}(r)?{phase:"participating",mechanism:"appointment",distribution:a,consumer:r}:"scheduled"===a.lifecycle?{phase:"scheduled",mechanism:"appointment",distribution:a}:"open"===a.lifecycle?{phase:"enterable",mechanism:"appointment",distribution:a,consumer:r}:s(a,r,{type:"closed"})}function n(t,e){return t&&"appointment"===t.mechanism&&e?"cancelled"===e.type?{...t,status:"cancelled"}:"no_show"===e.type?{...t,status:"no_show"}:"completed"===e.type?{...t,status:"completed"}:{...t,status:"not_booked"}:t}class o{constructor(t){this.deps=t,this.mechanism="appointment"}project(t){return i(t)}buildView(e,i){if("unavailable"===e.phase)return{phase:"unavailable",reason:e.reason};const n=function(e){if(!("mechanism"in e)||"appointment"!==e.mechanism)throw t.validationError("Appointment driver received a non-appointment sequence");return e}(e);switch(n.phase){case"scheduled":return{phase:"scheduled",mechanism:"appointment",distribution:n.distribution,startsAt:n.distribution.startsAt??n.distribution.opensAt};case"enterable":return{phase:"enterable",mechanism:"appointment",distribution:n.distribution,consumer:n.consumer,book:(t,e)=>this.book(t,e)};case"participating":return{phase:"participating",mechanism:"appointment",consumer:n.consumer,display$:i.displayAtom,cancel:t=>this.cancel(t),reschedule:(t,e)=>this.reschedule(t,e)};case"ended":return{phase:"ended",mechanism:"appointment",consumer:n.consumer,outcome:n.outcome}}}startMonitoring(t){this.activeMonitorId=t.participation.id,this.deps.appointments.startMonitoring(t.participation.id,{displayAtom:t.displayAtom},t.onUpdate,t.onDegradedChange)}stopMonitoring(){this.activeMonitorId&&(this.deps.appointments.stopMonitoring(this.activeMonitorId),this.activeMonitorId=void 0)}async book(t,e){await this.deps.runSerializedOperation(async()=>{const n=this.currentAppointmentSequence("enterable","No open appointment to book").distribution,o=await this.deps.appointments.book(n.id,t,e);this.deps.appointments.bumpMonitorGeneration(n.id),this.writeBookingDisplay(n.id,o),this.deps.commit(i({distribution:n,consumer:{mechanism:"appointment",status:"booked",id:n.id,distributionId:n.id,slotId:o.slotId,locationId:o.locationId}})),this.deps.emitEvent("sequence_change","success","user","Booked appointment slot",{appointmentId:n.id,slotId:t,locationId:e})})}async cancel(e){await this.deps.runSerializedOperation(async()=>{const i=this.currentAppointmentSequence("participating"),n=i.consumer,o=this.deps.getDisplayState(),s=o&&"appointment"===o.type?o.booking:void 0;if(!s)throw t.validationError("No booking details available to cancel");const a=n.distributionId??n.id??i.distribution.id;await this.deps.appointments.cancel(a,s.slotId,s.locationId??"",e),this.deps.stopRuntimeMonitoring(!0),this.deps.applyMonitorUpdate({type:"ended",outcome:{type:"cancelled",reason:e}})})}async reschedule(t,e){await this.deps.runSerializedOperation(async()=>{const i=this.currentAppointmentSequence("participating"),n=i.consumer,o=n.distributionId??n.id??i.distribution.id,s=await this.deps.appointments.reschedule(o,t,e);this.deps.appointments.bumpMonitorGeneration(o),this.writeBookingDisplay(o,s),this.deps.emitEvent("sequence_change","info","user","Rescheduled appointment",{appointmentId:o,newSlotId:t,newLocationId:e})})}writeBookingDisplay(t,e){const i=this.deps.ensureDisplayAtom({id:t,type:"appointment"}),n=Math.max(0,Date.parse(e.startTime)-Date.now());i.set({type:"appointment",booking:{slotId:e.slotId,locationId:e.locationId,locationName:e.locationName,joinUrl:e.joinUrl,startTime:e.startTime,endTime:e.endTime,confirmationCode:e.confirmationCode,consumerStatus:e.consumerStatus},msUntilStart:n})}currentAppointmentSequence(e,i="Not participating in an appointment"){const n=this.deps.getCurrentSequence();if(!n||n.phase!==e||!("mechanism"in n)||"appointment"!==n.mechanism)throw t.validationError(i);return n}}function s(t,e,i){return{phase:"ended",mechanism:"appointment",distribution:t,consumer:e,outcome:{...i,distributionType:i.distributionType??t.type,distributionId:i.distributionId??t.id,at:i.at??/* @__PURE__ */(new Date).toISOString()}}}export{e as createAppointmentDriver,i as projectAppointmentSequence,n as withAppointmentTerminalConsumer};
@@ -6,7 +6,7 @@ export declare class AppointmentManagementModule implements AppointmentModule {
6
6
  private readonly http;
7
7
  private readonly logger;
8
8
  private readonly monitorRuntime;
9
- private readonly pollingIntervals;
9
+ private readonly pollSchedulers;
10
10
  private readonly monitorGenerations;
11
11
  private readonly pollingIntervalMs;
12
12
  private destroyed;
@@ -23,7 +23,7 @@ export declare class AppointmentManagementModule implements AppointmentModule {
23
23
  book(appointmentId: string, slotId: string, locationId?: string): Promise<AppointmentBooking>;
24
24
  cancel(appointmentId: string, slotId: string, locationId?: string, reason?: string): Promise<void>;
25
25
  reschedule(appointmentId: string, newSlotId: string, newLocationId?: string): Promise<AppointmentBooking>;
26
- startMonitoring(id: string, context?: Record<string, unknown>, onUpdate?: (update: MonitorUpdate) => void): void;
26
+ startMonitoring(id: string, context?: Record<string, unknown>, onUpdate?: (update: MonitorUpdate) => void, onDegradedChange?: (degraded: boolean) => void): void;
27
27
  stopMonitoring(id: string): void;
28
28
  isMonitoring(id: string): boolean;
29
29
  bumpMonitorGeneration(id: string): void;
@@ -1 +1 @@
1
- import*as t from"valibot";import{isFanfareError as o,createError as n}from"../core/errors.js";import{getLogger as i}from"../core/logger.js";import{parseResponse as e}from"../core/parse-response.js";import{DistributionMonitorRuntime as r}from"../experiences/distribution-monitor.runtime.js";function s(t,o,n){const i=e(t,o,{endpoint:n},{throwOnFailure:!0});if(!i.ok)throw i.error;return i.value}const a=t.object({id:t.string(),startTime:t.string(),endTime:t.string(),locationId:t.nullish(t.string()),locationName:t.nullish(t.string()),timezone:t.nullish(t.string()),capacity:t.number(),available:t.number()}),l=t.object({slots:t.array(a)}),d=t.object({id:t.string(),type:t.literal("appointment"),openAt:t.nullish(t.string()),closeAt:t.nullish(t.string()),slotDurationMinutes:t.nullish(t.number()),timeZone:t.nullish(t.string()),locationId:t.nullish(t.string()),totalSlots:t.nullish(t.number()),availableSlots:t.nullish(t.number())}),m=t.picklist(["booked","checked_in","completed"]),c=t.object({bookingId:t.string(),appointmentId:t.string(),consumerId:t.string(),slotId:t.string(),locationId:t.nullish(t.string()),locationName:t.nullish(t.string()),startTime:t.string(),endTime:t.string(),confirmationCode:t.string(),status:m,createdAt:t.string()}),p=t.object({booking:t.nullish(t.object({bookingId:t.string(),appointmentId:t.string(),consumerId:t.string(),slotId:t.string(),locationId:t.nullish(t.string()),locationName:t.nullish(t.string()),joinUrl:t.nullish(t.string()),startTime:t.string(),endTime:t.string(),confirmationCode:t.nullish(t.string()),status:m}))}),g=t.object({bookingId:t.string(),previousSlotId:t.string(),newSlotId:t.string(),confirmationCode:t.string(),status:m});function h(t){return{id:t.id,startTime:t.startTime,endTime:t.endTime,locationId:t.locationId??void 0,locationName:t.locationName??void 0,timezone:t.timezone??void 0,capacity:t.capacity,available:t.available}}class u{constructor(t,o){this.logger=i(),this.monitorRuntime=new r,this.pollingIntervals=/* @__PURE__ */new Map,this.monitorGenerations=/* @__PURE__ */new Map,this.destroyed=!1,this.http=t,this.pollingIntervalMs=o?.pollingIntervalMs??3e4}async get(t){try{const o=await this.http.get(`/appointments/${t}`),n=s(d,o,`/appointments/${t}`);return{id:n.id,type:"appointment",openAt:n.openAt??void 0,closeAt:n.closeAt??void 0,slotDurationMinutes:n.slotDurationMinutes??void 0,bookingWindowOpenAt:n.openAt??void 0,bookingWindowCloseAt:n.closeAt??void 0,timezone:n.timeZone??void 0,locationId:n.locationId??void 0,totalSlots:n.totalSlots??void 0,availableSlots:n.availableSlots??void 0}}catch(i){if(this.logger.error("Failed to get appointment",{appointmentId:t,error:i}),o(i))throw i;throw n.networkError("Failed to get appointment",{error:i})}}async getSlots(t,i){try{const o=new URLSearchParams({startDate:i.startDate,endDate:i.endDate});i.timezone&&o.set("timezone",i.timezone);const n=await this.http.get(`/appointments/${t}/slots?${o.toString()}`);return s(l,n,`/appointments/${t}/slots`).slots.map(h)}catch(e){if(this.logger.error("Failed to get appointment slots",{appointmentId:t,error:e}),o(e))throw e;throw n.networkError("Failed to get appointment slots",{error:e})}}async getMe(t){try{const o=await this.http.get(`/appointments/${t}/me`),n=s(p,o,`/appointments/${t}/me`);return n.booking?function(t){return{bookingId:t.bookingId,appointmentId:t.appointmentId,slotId:t.slotId,locationId:t.locationId??void 0,locationName:t.locationName??void 0,joinUrl:t.joinUrl??void 0,startTime:t.startTime,endTime:t.endTime,confirmationCode:t.confirmationCode??void 0,consumerStatus:t.status}}(n.booking):null}catch(i){if(this.logger.error("Failed to get current booking",{appointmentId:t,error:i}),o(i))throw i;throw n.networkError("Failed to get current booking",{error:i})}}async book(t,i,e){try{const o=await this.http.post(`/appointments/${t}/book`,{slotId:i,locationId:e??null});return function(t){return{bookingId:t.bookingId,appointmentId:t.appointmentId,slotId:t.slotId,locationId:t.locationId??void 0,locationName:t.locationName??void 0,startTime:t.startTime,endTime:t.endTime,confirmationCode:t.confirmationCode,consumerStatus:t.status}}(s(c,o,`/appointments/${t}/book`))}catch(r){if(this.logger.error("Failed to book appointment",{appointmentId:t,slotId:i,locationId:e,error:r}),o(r))throw r;throw n.networkError("Failed to book appointment",{error:r})}}async cancel(t,i,e,r){try{await this.http.post(`/appointments/${t}/cancel`,{slotId:i,locationId:e??null,reason:r??null})}catch(s){if(this.logger.error("Failed to cancel appointment",{appointmentId:t,slotId:i,locationId:e,error:s}),o(s))throw s;throw n.networkError("Failed to cancel appointment",{error:s})}}async reschedule(t,i,e){try{const o=await this.http.post(`/appointments/${t}/reschedule`,{newSlotId:i,newLocationId:e??null}),n=s(g,o,`/appointments/${t}/reschedule`),r=await this.getMe(t);return r?{...r,confirmationCode:n.confirmationCode}:{bookingId:n.bookingId,appointmentId:t,slotId:n.newSlotId,locationId:e,startTime:"",endTime:"",confirmationCode:n.confirmationCode,consumerStatus:n.status}}catch(r){if(this.logger.error("Failed to reschedule appointment",{appointmentId:t,newSlotId:i,newLocationId:e,error:r}),o(r))throw r;throw n.networkError("Failed to reschedule appointment",{error:r})}}startMonitoring(t,o,n){if(this.destroyed)return;this.monitorRuntime.start(t,o,n),this.stopPolling(t);const i=(this.monitorGenerations.get(t)??0)+1;this.monitorGenerations.set(t,i);const e=async()=>{if(!this.destroyed&&this.monitorGenerations.get(t)===i)try{const o=await this.getMe(t);if(this.destroyed||this.monitorGenerations.get(t)!==i)return;const n=this.monitorRuntime.getDisplayAtom(t);if(!n)return;if(o){const t=Math.max(0,Date.parse(o.startTime)-Date.now());n.set({type:"appointment",booking:{slotId:o.slotId,locationId:o.locationId,locationName:o.locationName,joinUrl:o.joinUrl,startTime:o.startTime,endTime:o.endTime,confirmationCode:o.confirmationCode,consumerStatus:o.consumerStatus},msUntilStart:t})}else n.set({type:"appointment"})}catch(o){this.logger.error("Appointment polling tick failed",{id:t,error:o})}};e();const r=setInterval(()=>{e()},this.pollingIntervalMs);this.pollingIntervals.set(t,r)}stopMonitoring(t){this.stopPolling(t),this.monitorRuntime.stop(t),this.monitorGenerations.set(t,(this.monitorGenerations.get(t)??0)+1)}isMonitoring(t){return this.monitorRuntime.has(t)||this.pollingIntervals.has(t)}bumpMonitorGeneration(t){this.monitorGenerations.set(t,(this.monitorGenerations.get(t)??0)+1)}destroy(){this.destroyed=!0,this.monitorGenerations.clear();for(const t of Array.from(this.pollingIntervals.keys()))this.stopPolling(t);this.monitorRuntime.clear()}stopPolling(t){const o=this.pollingIntervals.get(t);void 0!==o&&(clearInterval(o),this.pollingIntervals.delete(t))}}export{u as AppointmentManagementModule};
1
+ import*as t from"valibot";import{isFanfareError as o,createError as i}from"../core/errors.js";import{getLogger as n}from"../core/logger.js";import{parseResponse as e}from"../core/parse-response.js";import{createPollScheduler as r}from"../core/poll-scheduler.js";import{DistributionMonitorRuntime as s}from"../experiences/distribution-monitor.runtime.js";function a(t,o,i){const n=e(t,o,{endpoint:i},{throwOnFailure:!0});if(!n.ok)throw n.error;return n.value}const l=t.object({id:t.string(),startTime:t.string(),endTime:t.string(),locationId:t.nullish(t.string()),locationName:t.nullish(t.string()),timezone:t.nullish(t.string()),capacity:t.number(),available:t.number()}),d=t.object({slots:t.array(l)}),m=t.object({id:t.string(),type:t.literal("appointment"),openAt:t.nullish(t.string()),closeAt:t.nullish(t.string()),slotDurationMinutes:t.nullish(t.number()),timeZone:t.nullish(t.string()),locationId:t.nullish(t.string()),totalSlots:t.nullish(t.number()),availableSlots:t.nullish(t.number())}),c=t.picklist(["booked","checked_in","completed"]),p=t.object({bookingId:t.string(),appointmentId:t.string(),consumerId:t.string(),slotId:t.string(),locationId:t.nullish(t.string()),locationName:t.nullish(t.string()),startTime:t.string(),endTime:t.string(),confirmationCode:t.string(),status:c,createdAt:t.string()}),h=t.object({booking:t.nullish(t.object({bookingId:t.string(),appointmentId:t.string(),consumerId:t.string(),slotId:t.string(),locationId:t.nullish(t.string()),locationName:t.nullish(t.string()),joinUrl:t.nullish(t.string()),startTime:t.string(),endTime:t.string(),confirmationCode:t.nullish(t.string()),status:c}))}),g=t.object({bookingId:t.string(),previousSlotId:t.string(),newSlotId:t.string(),confirmationCode:t.string(),status:c});function u(t){return{id:t.id,startTime:t.startTime,endTime:t.endTime,locationId:t.locationId??void 0,locationName:t.locationName??void 0,timezone:t.timezone??void 0,capacity:t.capacity,available:t.available}}class I{constructor(t,o){this.logger=n(),this.monitorRuntime=new s,this.pollSchedulers=/* @__PURE__ */new Map,this.monitorGenerations=/* @__PURE__ */new Map,this.destroyed=!1,this.http=t,this.pollingIntervalMs=o?.pollingIntervalMs??3e4}async get(t){try{const o=await this.http.get(`/appointments/${t}`),i=a(m,o,`/appointments/${t}`);return{id:i.id,type:"appointment",openAt:i.openAt??void 0,closeAt:i.closeAt??void 0,slotDurationMinutes:i.slotDurationMinutes??void 0,bookingWindowOpenAt:i.openAt??void 0,bookingWindowCloseAt:i.closeAt??void 0,timezone:i.timeZone??void 0,locationId:i.locationId??void 0,totalSlots:i.totalSlots??void 0,availableSlots:i.availableSlots??void 0}}catch(n){if(this.logger.error("Failed to get appointment",{appointmentId:t,error:n}),o(n))throw n;throw i.networkError("Failed to get appointment",{error:n})}}async getSlots(t,n){try{const o=new URLSearchParams({startDate:n.startDate,endDate:n.endDate});n.timezone&&o.set("timezone",n.timezone);const i=await this.http.get(`/appointments/${t}/slots?${o.toString()}`);return a(d,i,`/appointments/${t}/slots`).slots.map(u)}catch(e){if(this.logger.error("Failed to get appointment slots",{appointmentId:t,error:e}),o(e))throw e;throw i.networkError("Failed to get appointment slots",{error:e})}}async getMe(t){try{const o=await this.http.get(`/appointments/${t}/me`),i=a(h,o,`/appointments/${t}/me`);return i.booking?function(t){return{bookingId:t.bookingId,appointmentId:t.appointmentId,slotId:t.slotId,locationId:t.locationId??void 0,locationName:t.locationName??void 0,joinUrl:t.joinUrl??void 0,startTime:t.startTime,endTime:t.endTime,confirmationCode:t.confirmationCode??void 0,consumerStatus:t.status}}(i.booking):null}catch(n){if(this.logger.error("Failed to get current booking",{appointmentId:t,error:n}),o(n))throw n;throw i.networkError("Failed to get current booking",{error:n})}}async book(t,n,e){try{const o=await this.http.post(`/appointments/${t}/book`,{slotId:n,locationId:e??null});return function(t){return{bookingId:t.bookingId,appointmentId:t.appointmentId,slotId:t.slotId,locationId:t.locationId??void 0,locationName:t.locationName??void 0,startTime:t.startTime,endTime:t.endTime,confirmationCode:t.confirmationCode,consumerStatus:t.status}}(a(p,o,`/appointments/${t}/book`))}catch(r){if(this.logger.error("Failed to book appointment",{appointmentId:t,slotId:n,locationId:e,error:r}),o(r))throw r;throw i.networkError("Failed to book appointment",{error:r})}}async cancel(t,n,e,r){try{await this.http.post(`/appointments/${t}/cancel`,{slotId:n,locationId:e??null,reason:r??null})}catch(s){if(this.logger.error("Failed to cancel appointment",{appointmentId:t,slotId:n,locationId:e,error:s}),o(s))throw s;throw i.networkError("Failed to cancel appointment",{error:s})}}async reschedule(t,n,e){try{const o=await this.http.post(`/appointments/${t}/reschedule`,{newSlotId:n,newLocationId:e??null}),i=a(g,o,`/appointments/${t}/reschedule`),r=await this.getMe(t);return r?{...r,confirmationCode:i.confirmationCode}:{bookingId:i.bookingId,appointmentId:t,slotId:i.newSlotId,locationId:e,startTime:"",endTime:"",confirmationCode:i.confirmationCode,consumerStatus:i.status}}catch(r){if(this.logger.error("Failed to reschedule appointment",{appointmentId:t,newSlotId:n,newLocationId:e,error:r}),o(r))throw r;throw i.networkError("Failed to reschedule appointment",{error:r})}}startMonitoring(t,o,i,n){if(this.destroyed)return;this.monitorRuntime.start(t,o,i),this.stopPolling(t);const e=(this.monitorGenerations.get(t)??0)+1;this.monitorGenerations.set(t,e);const s=r({baseMs:this.pollingIntervalMs,suppressBackoff:()=>!1,onDegradedChange:n,shouldContinue:()=>!this.destroyed&&this.monitorGenerations.get(t)===e&&this.pollSchedulers.has(t),run:async()=>{if(!this.destroyed&&this.monitorGenerations.get(t)===e)try{const o=await this.getMe(t);if(this.destroyed||this.monitorGenerations.get(t)!==e)return;const i=this.monitorRuntime.getDisplayAtom(t);if(!i)return;if(o){const t=Math.max(0,Date.parse(o.startTime)-Date.now());i.set({type:"appointment",booking:{slotId:o.slotId,locationId:o.locationId,locationName:o.locationName,joinUrl:o.joinUrl,startTime:o.startTime,endTime:o.endTime,confirmationCode:o.confirmationCode,consumerStatus:o.consumerStatus},msUntilStart:t})}else i.set({type:"appointment"})}catch(o){throw this.logger.error("Appointment polling tick failed",{id:t,error:o}),o}}});this.pollSchedulers.set(t,s),s.start()}stopMonitoring(t){this.stopPolling(t),this.monitorRuntime.stop(t),this.monitorGenerations.set(t,(this.monitorGenerations.get(t)??0)+1)}isMonitoring(t){return this.monitorRuntime.has(t)||this.pollSchedulers.has(t)}bumpMonitorGeneration(t){this.monitorGenerations.set(t,(this.monitorGenerations.get(t)??0)+1)}destroy(){this.destroyed=!0,this.monitorGenerations.clear();for(const t of Array.from(this.pollSchedulers.keys()))this.stopPolling(t);this.monitorRuntime.clear()}stopPolling(t){const o=this.pollSchedulers.get(t);void 0!==o&&(o.stop(),this.pollSchedulers.delete(t))}}export{I as AppointmentManagementModule};
@@ -1,7 +1,7 @@
1
1
  import { AppointmentConsumerStatus, SequenceOutcome } from '@fanfare-io/fanfare-sdk-contracts/consumer-me';
2
2
  import { ReadableAtom } from 'nanostores';
3
3
  import { AppointmentDisplayState } from '../experiences/distribution-monitor.types';
4
- import { DistributionSummary, WaitlistAttachment, WaitlistAttachmentView } from '../experiences/journey.types';
4
+ import { DistributionSummary } from '../experiences/journey.types';
5
5
  /**
6
6
  * Appointment booking facts for one consumer.
7
7
  *
@@ -51,8 +51,6 @@ export interface AppointmentScheduled {
51
51
  distribution: DistributionSummary & {
52
52
  type: "appointment";
53
53
  };
54
- /** Scheduled-only waitlist attachment, cleared when the appointment opens. */
55
- waitlist?: WaitlistAttachment;
56
54
  }
57
55
  /**
58
56
  * Booking is open and this consumer has not booked an appointment.
@@ -123,8 +121,6 @@ export interface AppointmentScheduledView {
123
121
  };
124
122
  /** ISO 8601 opening timestamp chosen from the distribution start/open time. */
125
123
  startsAt?: string;
126
- /** Scheduled-only waitlist actions; this attachment never grants access. */
127
- waitlist?: WaitlistAttachmentView;
128
124
  }
129
125
  /**
130
126
  * Enterable appointment view rendered when the consumer can book a slot.
@@ -33,7 +33,7 @@ export interface AppointmentModule {
33
33
  book(appointmentId: string, slotId: string, locationId?: string): Promise<AppointmentBooking>;
34
34
  cancel(appointmentId: string, slotId: string, locationId?: string, reason?: string): Promise<void>;
35
35
  reschedule(appointmentId: string, newSlotId: string, newLocationId?: string): Promise<AppointmentBooking>;
36
- startMonitoring(id: string, context?: Record<string, unknown>, onUpdate?: (update: MonitorUpdate) => void): void;
36
+ startMonitoring(id: string, context?: Record<string, unknown>, onUpdate?: (update: MonitorUpdate) => void, onDegradedChange?: (degraded: boolean) => void): void;
37
37
  stopMonitoring(id: string): void;
38
38
  isMonitoring(id: string): boolean;
39
39
  /**
@@ -1 +1 @@
1
- import{createError as i}from"../core/errors.js";import{isMoneyString as t,addMoney as e}from"../core/money.js";function n(i){return new r(i)}function s(t){const{distribution:e,consumer:n,waitlist:s,grant:a,outcome:o}=t;if(!e)return{phase:"unavailable"};const r=function(t){if("auction"!==t.type)throw i.validationError("Auction driver received a non-auction distribution");return t}(e),d=n&&"auction"===n.mechanism?function(i){const{mechanism:t,...e}=i;return e}(n):u(e);if(a)return"completed"===o?.type?c(r,d,o):a.expiresAt&&Date.now()>new Date(a.expiresAt).getTime()?"open"===r.lifecycle?{phase:"enterable",mechanism:"auction",distribution:r,consumer:u(r)}:c(r,d,{type:"expired"}):{phase:"granted",mechanism:"auction",distribution:r,consumer:d,grant:a};if(!o&&"auction"===n?.mechanism&&"closed"===r.lifecycle)return c(r,d,{type:"lost"});const h=o??function(i){return"lost"===i.status?{type:"lost"}:void 0}(d);if(h)return c(r,d,h);if(function(i){return"bidding"===i.status||"winning"===i.status||"outbid"===i.status}(d))return{phase:"participating",mechanism:"auction",distribution:r,consumer:d};if("scheduled"===r.lifecycle){const i=s&&s.waitlistId===r.waitlistId?s:r.waitlistId?{waitlistId:r.waitlistId,status:"not_waitlisted"}:void 0;return{phase:"scheduled",mechanism:"auction",distribution:r,...i?{waitlist:i}:{}}}return"open"===r.lifecycle?{phase:"enterable",mechanism:"auction",distribution:r,consumer:d}:"settling"===r.lifecycle?{phase:"settling",mechanism:"auction",distribution:r,consumer:d}:c(r,d,{type:"closed"})}function a(i,t){return"auction"===i?.mechanism?{...i,status:"won"}:{mechanism:"auction",status:"won",id:t.id,distributionId:t.id}}function o(i,t){return i&&"auction"===i.mechanism&&t?"won"===t.type?{...i,status:"won"}:{...i,status:"lost"}:i}class r{constructor(i){this.deps=i,this.mechanism="auction"}project(i){return s(i)}buildView(t,e){if("unavailable"===t.phase)return{phase:"unavailable",reason:t.reason};const n=function(t){if(!("mechanism"in t)||"auction"!==t.mechanism)throw i.validationError("Auction driver received a non-auction sequence");return t}(t);switch(n.phase){case"scheduled":return{phase:"scheduled",mechanism:"auction",distribution:n.distribution,startsAt:n.distribution.startsAt??n.distribution.opensAt,waitlist:n.waitlist?{status:n.waitlist.status,join:()=>e.joinWaitlist(),leave:()=>e.exitWaitlist()}:void 0};case"enterable":return{phase:"enterable",mechanism:"auction",distribution:n.distribution,consumer:n.consumer,bid:i=>this.bid(i)};case"participating":return{phase:"participating",mechanism:"auction",consumer:n.consumer,display$:e.displayAtom,bid:i=>this.bid(i),leave:()=>this.leave()};case"settling":return{phase:"settling",mechanism:"auction",distribution:n.distribution,consumer:n.consumer};case"granted":return{phase:"granted",mechanism:"auction",distribution:n.distribution,consumer:n.consumer,grant:n.grant,claim:()=>e.claim()};case"ended":return{phase:"ended",mechanism:"auction",consumer:n.consumer,outcome:n.outcome}}}startMonitoring(i){this.activeMonitorId=i.participation.id,this.deps.auctions.startMonitoring(i.participation.id,{displayAtom:i.displayAtom},i.onUpdate)}stopMonitoring(){this.activeMonitorId&&(this.deps.auctions.stopMonitoring(this.activeMonitorId),this.activeMonitorId=void 0)}async enter(){await this.deps.runSerializedOperation(async()=>{await this.enterWithinOperation()})}async enterWithinOperation(){const i=this.currentAuctionSequence("enterable","No open auction to enter").distribution;await this.deps.auctions.enter(i.id,void 0),await this.deps.auctions.status(i.id);const t={mechanism:"auction",status:"bidding",id:e=i.id,distributionId:e};var e;this.deps.ensureDisplayAtom({id:i.id,type:"auction"}),this.deps.commit(s({distribution:i,consumer:t})),this.deps.emitEvent("sequence_change","success","user","Entered auction",{distributionId:i.id})}async leave(){await this.deps.runSerializedOperation(async()=>{await this.leaveWithinOperation()})}async leaveWithinOperation(){const i=this.currentAuctionSequence("participating"),t=i.consumer.distributionId??i.consumer.id??i.distribution.id;await this.deps.auctions.leave(t),this.deps.stopRuntimeMonitoring(!0),this.deps.applyMonitorUpdate({type:"ended",outcome:{type:"left"}}),this.deps.emitEvent("sequence_change","info","user","Left participation",{type:"auction"})}async bid(i){await this.deps.runSerializedOperation(async()=>{const n=this.currentAuctionSequenceForBid(),a=n.consumer.distributionId??n.consumer.id??n.distribution.id,o=await this.deps.auctions.bid(a,i),r="winning"===o.status?"winning":"outbid"===o.status?"outbid":"bidding",u="auction"===n.distribution.details?.type?n.distribution.details:void 0,c=u?.bidIncrement,d=o.highestBid&&c&&t(o.highestBid)&&t(c)?e(o.highestBid,c):void 0,h=this.deps.getDisplayState(),p="auction"===h?.type?h.settleAt:void 0,m=this.deps.ensureDisplayAtom({id:a,type:"auction"});"enterable"===n.phase&&this.deps.commit(s({distribution:n.distribution,consumer:{mechanism:"auction",status:r,id:a,distributionId:a,currentBid:i,highestBid:o.highestBid}})),m.set({type:"auction",currentBid:i,highestBid:o.highestBid,bidCount:o.bidCount,status:r,currencyCode:u?.currencyCode,bidIncrement:c,minNextBid:d,closeAt:u?.closeAt??u?.endsAt,settleAt:p})})}currentAuctionSequence(t,e="Not participating in an auction"){const n=this.deps.getCurrentSequence();if(!n||n.phase!==t||!("mechanism"in n)||"auction"!==n.mechanism)throw i.validationError(e);return n}currentAuctionSequenceForBid(){const t=this.deps.getCurrentSequence();if(!t||"enterable"!==t.phase&&"participating"!==t.phase||!("mechanism"in t)||"auction"!==t.mechanism)throw i.validationError("No auction is available to bid on");return t}}function u(i){return{status:"not_bid",distributionId:i.id}}function c(i,t,e){return{phase:"ended",mechanism:"auction",distribution:i,consumer:t,outcome:{...e,distributionType:e.distributionType??i.type,distributionId:e.distributionId??i.id,at:e.at??/* @__PURE__ */(new Date).toISOString()}}}export{n as createAuctionDriver,s as projectAuctionSequence,a as withAuctionGrantedConsumer,o as withAuctionTerminalConsumer};
1
+ import{createError as i}from"../core/errors.js";import{isMoneyString as t,addMoney as e}from"../core/money.js";function n(i){return new r(i)}function s(t){const{distribution:e,consumer:n,grant:s,outcome:o}=t;if(!e)return{phase:"unavailable"};const a=function(t){if("auction"!==t.type)throw i.validationError("Auction driver received a non-auction distribution");return t}(e),r=n&&"auction"===n.mechanism?function(i){const{mechanism:t,...e}=i;return e}(n):c(e);if(s)return"completed"===o?.type?u(a,r,o):s.expiresAt&&Date.now()>new Date(s.expiresAt).getTime()?"open"===a.lifecycle?{phase:"enterable",mechanism:"auction",distribution:a,consumer:c(a)}:u(a,r,{type:"expired"}):{phase:"granted",mechanism:"auction",distribution:a,consumer:r,grant:s};if(!o&&"auction"===n?.mechanism&&"closed"===a.lifecycle)return u(a,r,{type:"lost"});const d=o??function(i){return"lost"===i.status?{type:"lost"}:void 0}(r);return d?u(a,r,d):function(i){return"bidding"===i.status||"winning"===i.status||"outbid"===i.status}(r)?{phase:"participating",mechanism:"auction",distribution:a,consumer:r}:"scheduled"===a.lifecycle?{phase:"scheduled",mechanism:"auction",distribution:a}:"open"===a.lifecycle?{phase:"enterable",mechanism:"auction",distribution:a,consumer:r}:"settling"===a.lifecycle?{phase:"settling",mechanism:"auction",distribution:a,consumer:r}:u(a,r,{type:"closed"})}function o(i,t){return"auction"===i?.mechanism?{...i,status:"won"}:{mechanism:"auction",status:"won",id:t.id,distributionId:t.id}}function a(i,t){return i&&"auction"===i.mechanism&&t?"won"===t.type?{...i,status:"won"}:{...i,status:"lost"}:i}class r{constructor(i){this.deps=i,this.mechanism="auction"}project(i){return s(i)}buildView(t,e){if("unavailable"===t.phase)return{phase:"unavailable",reason:t.reason};const n=function(t){if(!("mechanism"in t)||"auction"!==t.mechanism)throw i.validationError("Auction driver received a non-auction sequence");return t}(t);switch(n.phase){case"scheduled":return{phase:"scheduled",mechanism:"auction",distribution:n.distribution,startsAt:n.distribution.startsAt??n.distribution.opensAt};case"enterable":return{phase:"enterable",mechanism:"auction",distribution:n.distribution,consumer:n.consumer,bid:i=>this.bid(i)};case"participating":return{phase:"participating",mechanism:"auction",consumer:n.consumer,display$:e.displayAtom,bid:i=>this.bid(i)};case"settling":return{phase:"settling",mechanism:"auction",distribution:n.distribution,consumer:n.consumer};case"granted":return{phase:"granted",mechanism:"auction",distribution:n.distribution,consumer:n.consumer,grant:n.grant,claim:()=>e.claim()};case"ended":return{phase:"ended",mechanism:"auction",consumer:n.consumer,outcome:n.outcome}}}startMonitoring(i){this.activeMonitorId=i.participation.id,this.deps.auctions.startMonitoring(i.participation.id,{displayAtom:i.displayAtom},i.onUpdate,i.onDegradedChange)}stopMonitoring(){this.activeMonitorId&&(this.deps.auctions.stopMonitoring(this.activeMonitorId),this.activeMonitorId=void 0)}async enter(){await this.deps.runSerializedOperation(async()=>{await this.enterWithinOperation()})}async enterWithinOperation(){const i=this.currentAuctionSequence("enterable","No open auction to enter").distribution;await this.deps.auctions.enter(i.id,void 0),await this.deps.auctions.status(i.id);const t={mechanism:"auction",status:"bidding",id:e=i.id,distributionId:e};var e;this.deps.ensureDisplayAtom({id:i.id,type:"auction"}),this.deps.commit(s({distribution:i,consumer:t})),this.deps.emitEvent("sequence_change","success","user","Entered auction",{distributionId:i.id})}async leave(){await this.deps.runSerializedOperation(async()=>{await this.leaveWithinOperation()})}async leaveWithinOperation(){const i=this.currentAuctionSequence("participating"),t=i.consumer.distributionId??i.consumer.id??i.distribution.id;await this.deps.auctions.leave(t),this.deps.stopRuntimeMonitoring(!0),this.deps.applyMonitorUpdate({type:"ended",outcome:{type:"left"}}),this.deps.emitEvent("sequence_change","info","user","Left participation",{type:"auction"})}async bid(i){await this.deps.runSerializedOperation(async()=>{const n=this.currentAuctionSequenceForBid(),o=n.consumer.distributionId??n.consumer.id??n.distribution.id,a=await this.deps.auctions.bid(o,i),r="winning"===a.status?"winning":"outbid"===a.status?"outbid":"bidding",c="auction"===n.distribution.details?.type?n.distribution.details:void 0,u=c?.bidIncrement,d=a.highestBid&&u&&t(a.highestBid)&&t(u)?e(a.highestBid,u):void 0,h=this.deps.getDisplayState(),p="auction"===h?.type?h.settleAt:void 0,m=this.deps.ensureDisplayAtom({id:o,type:"auction"});"enterable"===n.phase&&this.deps.commit(s({distribution:n.distribution,consumer:{mechanism:"auction",status:r,id:o,distributionId:o,currentBid:i,highestBid:a.highestBid}})),m.set({type:"auction",currentBid:i,highestBid:a.highestBid,bidCount:a.bidCount,status:r,currencyCode:c?.currencyCode,bidIncrement:u,minNextBid:d,closeAt:c?.closeAt??c?.endsAt,settleAt:p})})}currentAuctionSequence(t,e="Not participating in an auction"){const n=this.deps.getCurrentSequence();if(!n||n.phase!==t||!("mechanism"in n)||"auction"!==n.mechanism)throw i.validationError(e);return n}currentAuctionSequenceForBid(){const t=this.deps.getCurrentSequence();if(!t||"enterable"!==t.phase&&"participating"!==t.phase||!("mechanism"in t)||"auction"!==t.mechanism)throw i.validationError("No auction is available to bid on");return t}}function c(i){return{status:"not_bid",distributionId:i.id}}function u(i,t,e){return{phase:"ended",mechanism:"auction",distribution:i,consumer:t,outcome:{...e,distributionType:e.distributionType??i.type,distributionId:e.distributionId??i.id,at:e.at??/* @__PURE__ */(new Date).toISOString()}}}export{n as createAuctionDriver,s as projectAuctionSequence,o as withAuctionGrantedConsumer,a as withAuctionTerminalConsumer};
@@ -7,8 +7,11 @@ export declare class AuctionManagementModule implements AuctionModule {
7
7
  private readonly events;
8
8
  private readonly trackedParticipations;
9
9
  private readonly inFlightRequests;
10
- private readonly pollingIntervals;
10
+ private readonly pollSchedulers;
11
11
  private readonly lastStatusCache;
12
+ private readonly hasSuccessfulReadThisSession;
13
+ private readonly lastSettleAtMs;
14
+ private readonly visibilityHandlers;
12
15
  private readonly autoRebidConfigs;
13
16
  private readonly monitorRuntime;
14
17
  private readonly bidHistoryCache;
@@ -50,6 +53,10 @@ export declare class AuctionManagementModule implements AuctionModule {
50
53
  private startWatching;
51
54
  private stopWatching;
52
55
  private stopAllWatching;
56
+ private shouldSuppressBackoff;
57
+ private isInSettlingWindow;
58
+ private registerVisibilityPoke;
59
+ private unregisterVisibilityPoke;
53
60
  private bumpMonitorGeneration;
54
61
  private getOperationGeneration;
55
62
  private bumpOperationGeneration;
@@ -58,7 +65,7 @@ export declare class AuctionManagementModule implements AuctionModule {
58
65
  disableAutoRebid(auctionId: string): void;
59
66
  getAutoRebidConfig(auctionId: string): AutoRebidConfig | undefined;
60
67
  private handleAutoRebid;
61
- startMonitoring(id: string, context?: Record<string, unknown>, onUpdate?: (update: MonitorUpdate) => void): void;
68
+ startMonitoring(id: string, context?: Record<string, unknown>, onUpdate?: (update: MonitorUpdate) => void, onDegradedChange?: (degraded: boolean) => void): void;
62
69
  stopMonitoring(id: string): void;
63
70
  isMonitoring(id: string): boolean;
64
71
  destroy(): void;
@@ -1 +1 @@
1
- import{AuctionBidderStateResponseSchema as t,AuctionHighestBidResponseSchema as i,AuctionBidHistoryResponseSchema as e}from"@fanfare-io/fanfare-sdk-contracts/auction";import{createError as n}from"../core/errors.js";import{getLogger as a}from"../core/logger.js";import{isMoneyString as s,addMoney as o,isMoneyGreaterThan as r,normalizeMoney as u,isMoneyLessThanOrEqualTo as d,subtractMoney as c}from"../core/money.js";import{parseResponse as h}from"../core/parse-response.js";import{DistributionMonitorRuntime as l}from"../experiences/distribution-monitor.runtime.js";import{getEventBus as g}from"../state/events.js";import{getSDKStore as p}from"../state/store.js";class m{constructor(t){this.logger=a(),this.events=g(),this.trackedParticipations=/* @__PURE__ */new Map,this.inFlightRequests=/* @__PURE__ */new Map,this.pollingIntervals=/* @__PURE__ */new Map,this.lastStatusCache=/* @__PURE__ */new Map,this.autoRebidConfigs=/* @__PURE__ */new Map,this.monitorRuntime=new l,this.bidHistoryCache=/* @__PURE__ */new Map,this.auctionDetailsCache=/* @__PURE__ */new Map,this.monitorGenerations=/* @__PURE__ */new Map,this.operationGenerations=/* @__PURE__ */new Map,this.BID_HISTORY_CACHE_TTL=5e3,this.AUCTION_DETAILS_CACHE_TTL=3e4,this.NO_BID_END_GRACE_MS=5e3,this.destroyed=!1,this.lifecycleGeneration=0,this.http=t}get store(){return p()}computeMinNextBid(t,i){if(i&&s(t)&&s(i))return o(t,i)}async getAuctionDetailsCached(t){const i=this.auctionDetailsCache.get(t);if(i&&Date.now()-i.fetchedAt<this.AUCTION_DETAILS_CACHE_TTL)return i.details;const e=await this.http.get(`/auctions/${t}`);return this.auctionDetailsCache.set(t,{details:e,fetchedAt:Date.now()}),e}getTrackedParticipation(t){return this.trackedParticipations.get(t)}getAuctionCloseAt(t){return t.closeAt??t.settleAt}getAuctionSettleAt(t){return t.settleAt??t.closeAt}buildDisplayState(t,{status:i,highestBid:e,currentBid:n,bidCount:a}){return{highestBid:e,currentBid:n,bidCount:a,closeAt:this.getAuctionCloseAt(t),settleAt:t.settleAt,status:i,currencyCode:t.currencyCode,minNextBid:e?this.computeMinNextBid(e,t.minBidIncrement):void 0,bidIncrement:t.minBidIncrement??void 0}}emitTransitionEvents(t){for(const i of t)this.events.emit(i.type,i.payload)}applyTransition(t,i){i.trackedParticipation?this.trackedParticipations.set(t,i.trackedParticipation):this.trackedParticipations.delete(t);const e=this.monitorRuntime.getDisplayAtom(t);e&&i.displayState&&e.set({type:"auction",...i.displayState}),this.emitTransitionEvents(i.events),i.monitorUpdate&&this.monitorRuntime.notify(t,i.monitorUpdate),i.stopWatching&&this.stopWatching(t),i.autoRebidStatus&&this.handleAutoRebid(t,i.autoRebidStatus)}checkOutbidConditions(t,i,e){if("winning"===t.status&&"outbid"===i.status)return!0;if(e?.currentBid&&i.highestBid){const n=e.currentBid,a=i.highestBid,o=t.highestBid||"0";if(s(n)&&s(a)&&s(o))return r(a,o)&&r(a,n)&&"winning"!==i.status}return"outbid"!==t.status&&"outbid"===i.status}buildBidTransition(t,i,e,n,a){const s="winning"===i.status?"winning":"outbid"===i.status?"outbid":"bidding";return{trackedParticipation:{auctionId:t,enteredAt:a?.enteredAt??/* @__PURE__ */(new Date).toISOString(),status:s,currentBid:e,highestBid:i.highestBid,bidCount:i.bidCount,lastBidAt:/* @__PURE__ */(new Date).toISOString(),metadata:a?.metadata,fingerprint:a?.fingerprint},displayState:this.buildDisplayState(n,{status:i.status,highestBid:i.highestBid,currentBid:e,bidCount:i.bidCount}),events:[{type:"auction:bid-placed",payload:{auctionId:t,amount:e,status:i.status}},..."winning"===i.status?[{type:"auction:winning",payload:{auctionId:t,amount:e,highestBid:i.highestBid}}]:[],..."outbid"===i.status?[{type:"auction:outbid",payload:{auctionId:t,yourBid:e,highestBid:i.highestBid}}]:[]],monitorUpdate:null,stopWatching:!1,autoRebidStatus:null}}buildStatusTransition(t,i,e,{previousParticipation:n,previousStatus:a}){const s=i.currentBid??n?.currentBid,o=i.bidCount??n?.bidCount;if("won"===i.terminal)return{trackedParticipation:null,displayState:this.buildDisplayState(e,{status:"won",highestBid:i.highestBid,currentBid:s,bidCount:o}),events:[{type:"auction:won",payload:{auctionId:t,winningBid:s??""}}],monitorUpdate:i.admissionGrant?{type:"granted",token:i.admissionGrant}:{type:"ended",outcome:{type:"won",distributionType:"auction",distributionId:t}},stopWatching:!0,autoRebidStatus:null};if("lost"===i.terminal)return{trackedParticipation:null,displayState:this.buildDisplayState(e,{status:"lost",highestBid:i.highestBid,currentBid:s,bidCount:o}),events:[{type:"auction:lost",payload:{auctionId:t,highestBid:i.highestBid}}],monitorUpdate:{type:"ended",outcome:{type:"lost"}},stopWatching:!0,autoRebidStatus:null};const r=e.closeAt?new Date(e.closeAt).getTime():void 0;if("watching"===i.status&&void 0!==r&&Date.now()>=r+this.NO_BID_END_GRACE_MS)return{trackedParticipation:null,displayState:this.buildDisplayState(e,{status:"ended",highestBid:i.highestBid,currentBid:s,bidCount:o}),events:[],monitorUpdate:{type:"ended",outcome:{type:"closed",reason:"ended"}},stopWatching:!0,autoRebidStatus:null};const u={auctionId:t,enteredAt:n?.enteredAt??/* @__PURE__ */(new Date).toISOString(),status:i.status,currentBid:s,highestBid:i.highestBid,bidCount:o,metadata:n?.metadata,fingerprint:n?.fingerprint},d=[];let c=null;return a&&(this.checkOutbidConditions(a,i,n)&&(d.push({type:"auction:outbid",payload:{auctionId:t,yourBid:a.currentBid||n?.currentBid||"",highestBid:i.highestBid}}),c=i),"winning"!==a.status&&"winning"===i.status&&d.push({type:"auction:winning",payload:{auctionId:t,amount:s||"",highestBid:i.highestBid}}),a.highestBid!==i.highestBid&&"winning"!==i.status&&d.push({type:"auction:bid-updated",payload:{auctionId:t,highestBid:i.highestBid,bidCount:i.bidCount||0}})),{trackedParticipation:u,displayState:this.buildDisplayState(e,{status:i.status,highestBid:i.highestBid,currentBid:s,bidCount:o}),events:d,monitorUpdate:null,stopWatching:!1,autoRebidStatus:c}}buildMissingTransition(){return{trackedParticipation:null,displayState:{status:"lost"},events:[],monitorUpdate:{type:"ended",outcome:{type:"closed",reason:"not_participating"}},stopWatching:!0,autoRebidStatus:null}}async get(t){try{return await this.getAuctionDetailsCached(t)}catch(i){throw this.logger.error("Failed to get auction",{auctionId:t,error:i}),i}}async bid(e,a,o){try{const r=this.lifecycleGeneration;if(!s(a))throw n.validationError("Invalid bid amount format. Must be a numeric string (e.g., '100.00')");if(!this.store.session)throw n.unauthorized("Not authenticated. Call auth.guest() or auth.login() first");let d=this.bumpOperationGeneration(e),c=this.getTrackedParticipation(e);c||(this.logger.warn("Attempting to bid without entering auction first",{auctionId:e}),await this.enter(e,void 0,o),c=this.getTrackedParticipation(e),d=this.bumpOperationGeneration(e)),this.logger.info("Placing bid",{auctionId:e,amount:a});const l=`/auctions/${e}/bid`,g=`/auctions/${e}/highest-bid`,[p,m,b]=await Promise.all([this.http.post(l,{amount:a},o),this.http.get(g),this.getAuctionDetailsCached(e)]),y=h(t,p,{endpoint:l},{throwOnFailure:!0});if(!y.ok)throw y.error;const f=h(i,m,{endpoint:g},{throwOnFailure:!0});if(!f.ok)throw f.error;const I=y.value,B=f.value,w=u(B.amount),v={status:"WINNING"===I.status||"WON"===I.status?"winning":"OUTBID"===I.status||"BIDDING"===I.status||"LOST"===I.status?"outbid":"accepted",amount:a,highestBid:w,bidCount:I.bidCount??(c?.bidCount||0)+1};return this.canApplyAsyncResult(e,r,void 0,d)?(this.applyTransition(e,this.buildBidTransition(e,v,a,b,c)),v):v}catch(r){throw this.logger.error("Failed to place bid",{auctionId:e,amount:a,error:r}),this.events.emit("auction:error",{auctionId:e,error:r}),r}}async enter(t,i,e){try{const a=this.lifecycleGeneration,s=this.store.session;if(!s)throw n.unauthorized("Not authenticated. Call auth.guest() or auth.login() first");const o=this.bumpOperationGeneration(t);if(this.logger.info("Entering auction",{auctionId:t}),await this.http.post(`/auctions/${t}/enter`,{metadata:i},e),!this.canApplyAsyncResult(t,a,void 0,o))return;this.trackedParticipations.set(t,{auctionId:t,enteredAt:/* @__PURE__ */(new Date).toISOString(),status:"watching",metadata:i,fingerprint:s.deviceFingerprint}),this.emitTransitionEvents([{type:"auction:entered",payload:{auctionId:t}}])}catch(a){throw this.logger.error("Failed to enter auction",{auctionId:t,error:a}),this.events.emit("auction:error",{auctionId:t,error:a}),a}}async leave(t,i){try{const n=this.lifecycleGeneration,a=this.bumpOperationGeneration(t);this.logger.info("Leaving auction",{auctionId:t});try{await this.http.post(`/auctions/${t}/leave`,void 0,i)}catch(e){if(404!==e?.status)throw e;this.logger.debug("Leave endpoint not available",{auctionId:t})}if(!this.canApplyAsyncResult(t,n,void 0,a))return;this.stopMonitoring(t),this.trackedParticipations.delete(t),this.emitTransitionEvents([{type:"auction:left",payload:{auctionId:t}}])}catch(e){throw this.logger.error("Failed to leave auction",{auctionId:t,error:e}),this.events.emit("auction:error",{auctionId:t,error:e}),e}}async status(t){const i=this.inFlightRequests.get(t);if(i)return this.logger.debug("Returning existing in-flight request",{auctionId:t}),i;const e=this.lifecycleGeneration,n=this.bumpOperationGeneration(t),a=this.doStatus(t,e,void 0,n).finally(()=>{this.inFlightRequests.delete(t)});return this.inFlightRequests.set(t,a),a}async doStatus(e,n,a,s){try{const o=`/auctions/${e}/status`,r=`/auctions/${e}/highest-bid`,[d,c,l]=await Promise.all([this.http.get(o),this.http.get(r),this.getAuctionDetailsCached(e)]),g=h(t,d,{endpoint:o},{throwOnFailure:!0});if(!g.ok)throw g.error;const p=h(i,c,{endpoint:r},{throwOnFailure:!0});if(!p.ok)throw p.error;const m=g.value,b=p.value,y=this.getAuctionSettleAt(l),f=y?new Date(y).getTime():Number.NaN,I=u(b.amount),B=this.getTrackedParticipation(e),w=m.lastBidAmount??m.winningBidAmount;let v="watching";"WINNING"===m.status||"WON"===m.status?v="winning":"OUTBID"!==m.status&&"LOST"!==m.status&&"BIDDING"!==m.status||(v="outbid");const A="WON"===m.status?"won":"LOST"===m.status?"lost":void 0,C={auctionId:e,status:v,...A?{terminal:A}:{},currentBid:B?.currentBid??(w?u(w):void 0),highestBid:I,timeRemaining:Number.isFinite(f)?Math.max(0,f-Date.now()):0,bidCount:m.bidCount,admissionGrant:m.admissionGrant};if(!this.canApplyAsyncResult(e,n,a,s))return C;const S=this.buildStatusTransition(e,C,l,{previousParticipation:B,previousStatus:this.lastStatusCache.get(e)});return this.applyTransition(e,S),S.stopWatching?this.lastStatusCache.delete(e):this.lastStatusCache.set(e,C),this.events.emit("auction:status-updated",{auctionId:e,status:C}),C}catch(o){if(o&&"object"==typeof o&&"status"in o&&404===o.status){const t={auctionId:e,status:"watching",highestBid:"0.00",timeRemaining:0};if(!this.canApplyAsyncResult(e,n,a,s))return t;const i=this.buildMissingTransition();return this.applyTransition(e,i),this.lastStatusCache.delete(e),this.events.emit("auction:status-updated",{auctionId:e,status:t}),t}if(!this.canApplyAsyncResult(e,n,a,s))return this.lastStatusCache.get(e)??{auctionId:e,status:"watching",highestBid:"0.00",timeRemaining:0};throw this.logger.error("Failed to get auction status",{auctionId:e,error:o}),this.events.emit("auction:error",{auctionId:e,error:o}),o}}async getBidHistory(t){try{const i=this.bidHistoryCache.get(t);if(i&&Date.now()-i.fetchedAt<this.BID_HISTORY_CACHE_TTL)return this.logger.debug("Returning cached bid history",{auctionId:t}),i.bids;this.logger.info("Fetching bid history",{auctionId:t});const n=`/auctions/${t}/bids/history`,a=await this.http.get(n),s=h(e,a,{endpoint:n},{throwOnFailure:!0});if(!s.ok)throw s.error;const o=s.value.map(t=>({amount:u(t.amount),timestamp:t.timestamp,isWinning:t.isHighest,isYours:!0}));return this.bidHistoryCache.set(t,{bids:o,fetchedAt:Date.now()}),this.events.emit("auction:history-fetched",{auctionId:t,bidCount:o.length}),o}catch(i){throw this.logger.error("Failed to get bid history",{auctionId:t,error:i}),this.events.emit("auction:error",{auctionId:t,error:i}),i}}startWatching(t,i=5e3){this.stopWatching(t);const e=this.bumpMonitorGeneration(t);this.logger.info("Starting auction status polling",{auctionId:t,intervalMs:i});const n=this.getOperationGeneration(t);this.doStatus(t,this.lifecycleGeneration,e,n).catch(i=>{this.canApplyAsyncResult(t,this.lifecycleGeneration,e,n)&&this.logger.error("Initial status check failed",{auctionId:t,error:i})});const a=setInterval(()=>{const i=this.getOperationGeneration(t);this.doStatus(t,this.lifecycleGeneration,e,i).catch(n=>{this.canApplyAsyncResult(t,this.lifecycleGeneration,e,i)&&this.logger.error("Polling failed",{auctionId:t,error:n})})},i);this.pollingIntervals.set(t,a),this.events.emit("auction:watching-started",{auctionId:t,intervalMs:i})}stopWatching(t){this.bumpMonitorGeneration(t);const i=this.pollingIntervals.get(t);i&&(clearInterval(i),this.pollingIntervals.delete(t),this.lastStatusCache.delete(t),this.logger.info("Stopped watching auction",{auctionId:t}),this.events.emit("auction:watching-stopped",{auctionId:t}))}stopAllWatching(){for(const t of Array.from(this.pollingIntervals.keys()))this.stopWatching(t)}bumpMonitorGeneration(t){const i=(this.monitorGenerations.get(t)??0)+1;return this.monitorGenerations.set(t,i),i}getOperationGeneration(t){return this.operationGenerations.get(t)??0}bumpOperationGeneration(t){const i=this.getOperationGeneration(t)+1;return this.operationGenerations.set(t,i),i}canApplyAsyncResult(t,i,e,n){return!this.destroyed&&this.lifecycleGeneration===i&&((void 0===e||this.monitorGenerations.get(t)===e)&&(void 0===n||this.getOperationGeneration(t)===n))}enableAutoRebid(t,i,e){if(!s(i)||!s(e))throw n.validationError("Invalid monetary format. Must be numeric strings (e.g., '100.00')");if(d(i,"0")||d(e,"0"))throw n.validationError("Max bid and increment must be positive");if(r(e,i))throw n.validationError("Increment cannot be greater than max bid");const a=this.getTrackedParticipation(t);if(!a)throw n.validationError("Must enter auction before enabling auto-rebid");const o=u(a.currentBid),h=c(i,o);this.autoRebidConfigs.set(t,{enabled:!0,maxBid:i,increment:e,remainingBudget:h,rebidCount:0}),this.logger.info("Auto-rebid enabled",{auctionId:t,maxBid:i,increment:e,remainingBudget:h}),this.events.emit("auction:auto-rebid-enabled",{auctionId:t,maxBid:i,increment:e})}disableAutoRebid(t){const i=this.autoRebidConfigs.get(t);i&&(i.enabled=!1,this.logger.info("Auto-rebid disabled",{auctionId:t}),this.events.emit("auction:auto-rebid-disabled",{auctionId:t,finalRebidCount:i.rebidCount||0}))}getAutoRebidConfig(t){return this.autoRebidConfigs.get(t)}async handleAutoRebid(t,i){const e=this.autoRebidConfigs.get(t);if(e&&e.enabled)try{if(!s(i.highestBid)||!s(e.maxBid)||!s(e.increment))return void this.logger.error("Invalid auto-rebid amounts",{auctionId:t,config:e});const n=o(i.highestBid,e.increment);if(r(n,e.maxBid))return this.logger.info("Auto-rebid would exceed max bid, disabling",{auctionId:t,wouldBid:n,maxBid:e.maxBid}),this.disableAutoRebid(t),void this.events.emit("auction:auto-rebid-max-reached",{auctionId:t,maxBid:e.maxBid,wouldNeedToBid:n});this.logger.info("Placing auto-rebid",{auctionId:t,amount:n});const a=await this.bid(t,n);e.lastRebidAt=/* @__PURE__ */(new Date).toISOString(),e.rebidCount=(e.rebidCount||0)+1,e.remainingBudget=c(e.maxBid,n),this.events.emit("auction:auto-rebid-placed",{auctionId:t,amount:n,remainingBudget:e.remainingBudget,status:a.status})}catch(n){this.logger.error("Auto-rebid failed",{auctionId:t,error:n}),this.events.emit("auction:auto-rebid-failed",{auctionId:t,error:n})}}startMonitoring(t,i,e){this.monitorRuntime.start(t,i,e),this.trackedParticipations.has(t)||this.trackedParticipations.set(t,{auctionId:t,enteredAt:/* @__PURE__ */(new Date).toISOString(),status:"watching"}),this.startWatching(t)}stopMonitoring(t){this.stopWatching(t),this.monitorRuntime.stop(t)}isMonitoring(t){return this.monitorRuntime.has(t)||this.pollingIntervals.has(t)}destroy(){this.destroyed=!0,this.lifecycleGeneration+=1,this.stopAllWatching(),this.inFlightRequests.clear(),this.lastStatusCache.clear(),this.trackedParticipations.clear(),this.monitorGenerations.clear(),this.operationGenerations.clear(),this.bidHistoryCache.clear(),this.auctionDetailsCache.clear(),this.autoRebidConfigs.clear(),this.monitorRuntime.clear()}}export{m as AuctionManagementModule};
1
+ import{AuctionBidderStateResponseSchema as t,AuctionHighestBidResponseSchema as i,AuctionBidHistoryResponseSchema as e}from"@fanfare-io/fanfare-sdk-contracts/auction";import{createError as s}from"../core/errors.js";import{getLogger as n}from"../core/logger.js";import{isMoneyString as a,addMoney as o,isMoneyGreaterThan as r,normalizeMoney as u,isMoneyLessThanOrEqualTo as d,subtractMoney as h}from"../core/money.js";import{parseResponse as c}from"../core/parse-response.js";import{createPollScheduler as l}from"../core/poll-scheduler.js";import{DistributionMonitorRuntime as g}from"../experiences/distribution-monitor.runtime.js";import{getEventBus as p}from"../state/events.js";import{getSDKStore as m}from"../state/store.js";class b{constructor(t){this.logger=n(),this.events=p(),this.trackedParticipations=/* @__PURE__ */new Map,this.inFlightRequests=/* @__PURE__ */new Map,this.pollSchedulers=/* @__PURE__ */new Map,this.lastStatusCache=/* @__PURE__ */new Map,this.hasSuccessfulReadThisSession=/* @__PURE__ */new Map,this.lastSettleAtMs=/* @__PURE__ */new Map,this.visibilityHandlers=/* @__PURE__ */new Map,this.autoRebidConfigs=/* @__PURE__ */new Map,this.monitorRuntime=new g,this.bidHistoryCache=/* @__PURE__ */new Map,this.auctionDetailsCache=/* @__PURE__ */new Map,this.monitorGenerations=/* @__PURE__ */new Map,this.operationGenerations=/* @__PURE__ */new Map,this.BID_HISTORY_CACHE_TTL=5e3,this.AUCTION_DETAILS_CACHE_TTL=3e4,this.NO_BID_END_GRACE_MS=5e3,this.destroyed=!1,this.lifecycleGeneration=0,this.http=t}get store(){return m()}computeMinNextBid(t,i){if(i&&a(t)&&a(i))return o(t,i)}async getAuctionDetailsCached(t){const i=this.auctionDetailsCache.get(t);if(i&&Date.now()-i.fetchedAt<this.AUCTION_DETAILS_CACHE_TTL)return i.details;const e=await this.http.get(`/auctions/${t}`);return this.auctionDetailsCache.set(t,{details:e,fetchedAt:Date.now()}),e}getTrackedParticipation(t){return this.trackedParticipations.get(t)}getAuctionCloseAt(t){return t.closeAt??t.settleAt}getAuctionSettleAt(t){return t.settleAt??t.closeAt}buildDisplayState(t,{status:i,highestBid:e,currentBid:s,bidCount:n}){return{highestBid:e,currentBid:s,bidCount:n,closeAt:this.getAuctionCloseAt(t),settleAt:t.settleAt,status:i,currencyCode:t.currencyCode,minNextBid:e?this.computeMinNextBid(e,t.minBidIncrement):void 0,bidIncrement:t.minBidIncrement??void 0}}emitTransitionEvents(t){for(const i of t)this.events.emit(i.type,i.payload)}applyTransition(t,i){i.trackedParticipation?this.trackedParticipations.set(t,i.trackedParticipation):this.trackedParticipations.delete(t);const e=this.monitorRuntime.getDisplayAtom(t);e&&i.displayState&&e.set({type:"auction",...i.displayState}),this.emitTransitionEvents(i.events),i.monitorUpdate&&this.monitorRuntime.notify(t,i.monitorUpdate),i.stopWatching&&this.stopWatching(t),i.autoRebidStatus&&this.handleAutoRebid(t,i.autoRebidStatus)}checkOutbidConditions(t,i,e){if("winning"===t.status&&"outbid"===i.status)return!0;if(e?.currentBid&&i.highestBid){const s=e.currentBid,n=i.highestBid,o=t.highestBid||"0";if(a(s)&&a(n)&&a(o))return r(n,o)&&r(n,s)&&"winning"!==i.status}return"outbid"!==t.status&&"outbid"===i.status}buildBidTransition(t,i,e,s,n){const a="winning"===i.status?"winning":"outbid"===i.status?"outbid":"bidding";return{trackedParticipation:{auctionId:t,enteredAt:n?.enteredAt??/* @__PURE__ */(new Date).toISOString(),status:a,currentBid:e,highestBid:i.highestBid,bidCount:i.bidCount,lastBidAt:/* @__PURE__ */(new Date).toISOString(),metadata:n?.metadata,fingerprint:n?.fingerprint},displayState:this.buildDisplayState(s,{status:i.status,highestBid:i.highestBid,currentBid:e,bidCount:i.bidCount}),events:[{type:"auction:bid-placed",payload:{auctionId:t,amount:e,status:i.status}},..."winning"===i.status?[{type:"auction:winning",payload:{auctionId:t,amount:e,highestBid:i.highestBid}}]:[],..."outbid"===i.status?[{type:"auction:outbid",payload:{auctionId:t,yourBid:e,highestBid:i.highestBid}}]:[]],monitorUpdate:null,stopWatching:!1,autoRebidStatus:null}}buildStatusTransition(t,i,e,{previousParticipation:s,previousStatus:n}){const a=i.currentBid??s?.currentBid,o=i.bidCount??s?.bidCount;if("won"===i.terminal)return{trackedParticipation:null,displayState:this.buildDisplayState(e,{status:"won",highestBid:i.highestBid,currentBid:a,bidCount:o}),events:[{type:"auction:won",payload:{auctionId:t,winningBid:a??""}}],monitorUpdate:i.admissionGrant?{type:"granted",token:i.admissionGrant}:{type:"ended",outcome:{type:"won",distributionType:"auction",distributionId:t}},stopWatching:!0,autoRebidStatus:null};if("lost"===i.terminal)return{trackedParticipation:null,displayState:this.buildDisplayState(e,{status:"lost",highestBid:i.highestBid,currentBid:a,bidCount:o}),events:[{type:"auction:lost",payload:{auctionId:t,highestBid:i.highestBid}}],monitorUpdate:{type:"ended",outcome:{type:"lost"}},stopWatching:!0,autoRebidStatus:null};const r=e.closeAt?new Date(e.closeAt).getTime():void 0;if("watching"===i.status&&void 0!==r&&Date.now()>=r+this.NO_BID_END_GRACE_MS)return{trackedParticipation:null,displayState:this.buildDisplayState(e,{status:"ended",highestBid:i.highestBid,currentBid:a,bidCount:o}),events:[],monitorUpdate:{type:"ended",outcome:{type:"closed",reason:"ended"}},stopWatching:!0,autoRebidStatus:null};const u={auctionId:t,enteredAt:s?.enteredAt??/* @__PURE__ */(new Date).toISOString(),status:i.status,currentBid:a,highestBid:i.highestBid,bidCount:o,metadata:s?.metadata,fingerprint:s?.fingerprint},d=[];let h=null;return n&&(this.checkOutbidConditions(n,i,s)&&(d.push({type:"auction:outbid",payload:{auctionId:t,yourBid:n.currentBid||s?.currentBid||"",highestBid:i.highestBid}}),h=i),"winning"!==n.status&&"winning"===i.status&&d.push({type:"auction:winning",payload:{auctionId:t,amount:a||"",highestBid:i.highestBid}}),n.highestBid!==i.highestBid&&"winning"!==i.status&&d.push({type:"auction:bid-updated",payload:{auctionId:t,highestBid:i.highestBid,bidCount:i.bidCount||0}})),{trackedParticipation:u,displayState:this.buildDisplayState(e,{status:i.status,highestBid:i.highestBid,currentBid:a,bidCount:o}),events:d,monitorUpdate:null,stopWatching:!1,autoRebidStatus:h}}buildMissingTransition(){return{trackedParticipation:null,displayState:{status:"lost"},events:[],monitorUpdate:{type:"ended",outcome:{type:"closed",reason:"not_participating"}},stopWatching:!0,autoRebidStatus:null}}async get(t){try{return await this.getAuctionDetailsCached(t)}catch(i){throw this.logger.error("Failed to get auction",{auctionId:t,error:i}),i}}async bid(e,n,o){try{const r=this.lifecycleGeneration;if(!a(n))throw s.validationError("Invalid bid amount format. Must be a numeric string (e.g., '100.00')");if(!this.store.session)throw s.unauthorized("Not authenticated. Call auth.guest() or auth.login() first");let d=this.bumpOperationGeneration(e),h=this.getTrackedParticipation(e);h||(this.logger.warn("Attempting to bid without entering auction first",{auctionId:e}),await this.enter(e,void 0,o),h=this.getTrackedParticipation(e),d=this.bumpOperationGeneration(e)),this.logger.info("Placing bid",{auctionId:e,amount:n});const l=`/auctions/${e}/bid`,g=`/auctions/${e}/highest-bid`,[p,m,b]=await Promise.all([this.http.post(l,{amount:n},o),this.http.get(g),this.getAuctionDetailsCached(e)]),y=c(t,p,{endpoint:l},{throwOnFailure:!0});if(!y.ok)throw y.error;const f=c(i,m,{endpoint:g},{throwOnFailure:!0});if(!f.ok)throw f.error;const w=y.value,B=f.value,S=u(B.amount),I={status:"WINNING"===w.status||"WON"===w.status?"winning":"OUTBID"===w.status||"BIDDING"===w.status||"LOST"===w.status?"outbid":"accepted",amount:n,highestBid:S,bidCount:w.bidCount??(h?.bidCount||0)+1};return this.canApplyAsyncResult(e,r,void 0,d)?(this.applyTransition(e,this.buildBidTransition(e,I,n,b,h)),I):I}catch(r){throw this.logger.error("Failed to place bid",{auctionId:e,amount:n,error:r}),this.events.emit("auction:error",{auctionId:e,error:r}),r}}async enter(t,i,e){try{const n=this.lifecycleGeneration,a=this.store.session;if(!a)throw s.unauthorized("Not authenticated. Call auth.guest() or auth.login() first");const o=this.bumpOperationGeneration(t);if(this.logger.info("Entering auction",{auctionId:t}),await this.http.post(`/auctions/${t}/enter`,{metadata:i},e),!this.canApplyAsyncResult(t,n,void 0,o))return;this.trackedParticipations.set(t,{auctionId:t,enteredAt:/* @__PURE__ */(new Date).toISOString(),status:"watching",metadata:i,fingerprint:a.deviceFingerprint}),this.emitTransitionEvents([{type:"auction:entered",payload:{auctionId:t}}])}catch(n){throw this.logger.error("Failed to enter auction",{auctionId:t,error:n}),this.events.emit("auction:error",{auctionId:t,error:n}),n}}async leave(t,i){const e=this.getTrackedParticipation(t);if(e&&(void 0!==e.currentBid||"watching"!==e.status))throw s.cannotLeaveAfterBid(t);try{const e=this.lifecycleGeneration,s=this.bumpOperationGeneration(t);this.logger.info("Leaving auction",{auctionId:t});try{await this.http.post(`/auctions/${t}/leave`,void 0,i)}catch(n){if(404!==n?.status)throw n;this.logger.debug("Leave endpoint not available",{auctionId:t})}if(!this.canApplyAsyncResult(t,e,void 0,s))return;this.stopMonitoring(t),this.trackedParticipations.delete(t),this.emitTransitionEvents([{type:"auction:left",payload:{auctionId:t}}])}catch(n){throw this.logger.error("Failed to leave auction",{auctionId:t,error:n}),this.events.emit("auction:error",{auctionId:t,error:n}),n}}async status(t){const i=this.inFlightRequests.get(t);if(i)return this.logger.debug("Returning existing in-flight request",{auctionId:t}),i;const e=this.lifecycleGeneration,s=this.bumpOperationGeneration(t),n=this.doStatus(t,e,void 0,s).finally(()=>{this.inFlightRequests.delete(t)});return this.inFlightRequests.set(t,n),n}async doStatus(e,s,n,a){try{const o=`/auctions/${e}/status`,r=`/auctions/${e}/highest-bid`,[d,h,l]=await Promise.all([this.http.get(o),this.http.get(r),this.getAuctionDetailsCached(e)]),g=c(t,d,{endpoint:o},{throwOnFailure:!0});if(!g.ok)throw g.error;const p=c(i,h,{endpoint:r},{throwOnFailure:!0});if(!p.ok)throw p.error;const m=g.value,b=p.value,y=this.getAuctionSettleAt(l),f=y?new Date(y).getTime():Number.NaN,w=u(b.amount),B=this.getTrackedParticipation(e),S=m.lastBidAmount??m.winningBidAmount;let I="watching";"WINNING"===m.status||"WON"===m.status?I="winning":"OUTBID"!==m.status&&"LOST"!==m.status&&"BIDDING"!==m.status||(I="outbid");const v="WON"===m.status?"won":"LOST"===m.status?"lost":void 0,A={auctionId:e,status:I,...v?{terminal:v}:{},currentBid:B?.currentBid??(S?u(S):void 0),highestBid:w,timeRemaining:Number.isFinite(f)?Math.max(0,f-Date.now()):0,bidCount:m.bidCount,admissionGrant:m.admissionGrant};if(!this.canApplyAsyncResult(e,s,n,a))return A;const C=this.buildStatusTransition(e,A,l,{previousParticipation:B,previousStatus:this.lastStatusCache.get(e)});return this.applyTransition(e,C),this.hasSuccessfulReadThisSession.set(e,!0),Number.isFinite(f)?this.lastSettleAtMs.set(e,f):this.lastSettleAtMs.delete(e),C.stopWatching?this.lastStatusCache.delete(e):this.lastStatusCache.set(e,A),this.events.emit("auction:status-updated",{auctionId:e,status:A}),A}catch(o){if(o&&"object"==typeof o&&"status"in o&&404===o.status){const t={auctionId:e,status:"watching",highestBid:"0.00",timeRemaining:0};if(!this.canApplyAsyncResult(e,s,n,a))return t;const i=this.buildMissingTransition();return this.applyTransition(e,i),this.lastStatusCache.delete(e),this.hasSuccessfulReadThisSession.set(e,!0),this.lastSettleAtMs.delete(e),this.events.emit("auction:status-updated",{auctionId:e,status:t}),t}if(!this.canApplyAsyncResult(e,s,n,a))return this.lastStatusCache.get(e)??{auctionId:e,status:"watching",highestBid:"0.00",timeRemaining:0};throw this.logger.error("Failed to get auction status",{auctionId:e,error:o}),this.events.emit("auction:error",{auctionId:e,error:o}),o}}async getBidHistory(t){try{const i=this.bidHistoryCache.get(t);if(i&&Date.now()-i.fetchedAt<this.BID_HISTORY_CACHE_TTL)return this.logger.debug("Returning cached bid history",{auctionId:t}),i.bids;this.logger.info("Fetching bid history",{auctionId:t});const s=`/auctions/${t}/bids/history`,n=await this.http.get(s),a=c(e,n,{endpoint:s},{throwOnFailure:!0});if(!a.ok)throw a.error;const o=a.value.map(t=>({amount:u(t.amount),timestamp:t.timestamp,isWinning:t.isHighest,isYours:!0}));return this.bidHistoryCache.set(t,{bids:o,fetchedAt:Date.now()}),this.events.emit("auction:history-fetched",{auctionId:t,bidCount:o.length}),o}catch(i){throw this.logger.error("Failed to get bid history",{auctionId:t,error:i}),this.events.emit("auction:error",{auctionId:t,error:i}),i}}startWatching(t,i=5e3,e){this.stopWatching(t);const s=this.lifecycleGeneration,n=this.bumpMonitorGeneration(t);this.hasSuccessfulReadThisSession.set(t,!1),this.lastStatusCache.delete(t),this.lastSettleAtMs.delete(t),this.logger.info("Starting auction status polling",{auctionId:t,intervalMs:i});const a=l({baseMs:i,suppressBackoff:()=>this.shouldSuppressBackoff(t),onDegradedChange:e,shouldContinue:()=>this.pollSchedulers.has(t)&&this.canApplyAsyncResult(t,s,n),run:async()=>{const i=this.getOperationGeneration(t);try{await this.doStatus(t,s,n,i)}catch(e){if(!this.canApplyAsyncResult(t,s,n,i))return;throw this.logger.error("Polling failed",{auctionId:t,error:e}),e}}});this.pollSchedulers.set(t,a),this.registerVisibilityPoke(t,a),a.start(),this.events.emit("auction:watching-started",{auctionId:t,intervalMs:i})}stopWatching(t){this.bumpMonitorGeneration(t);const i=this.pollSchedulers.get(t);i&&(i.stop(),this.pollSchedulers.delete(t),this.unregisterVisibilityPoke(t),this.lastStatusCache.delete(t),this.hasSuccessfulReadThisSession.delete(t),this.lastSettleAtMs.delete(t),this.logger.info("Stopped watching auction",{auctionId:t}),this.events.emit("auction:watching-stopped",{auctionId:t}))}stopAllWatching(){for(const t of Array.from(this.pollSchedulers.keys()))this.stopWatching(t)}shouldSuppressBackoff(t){return!this.hasSuccessfulReadThisSession.get(t)||this.isInSettlingWindow(t)}isInSettlingWindow(t){const i=this.lastSettleAtMs.get(t);if(void 0===i)return!1;const e=this.lastStatusCache.get(t)?.terminal;return Date.now()>=i-12e4&&"won"!==e&&"lost"!==e}registerVisibilityPoke(t,i){if("undefined"==typeof document)return;const e=()=>{"visible"===document.visibilityState&&this.isInSettlingWindow(t)&&i.poke()};document.addEventListener("visibilitychange",e),this.visibilityHandlers.set(t,e)}unregisterVisibilityPoke(t){if("undefined"==typeof document)return void this.visibilityHandlers.delete(t);const i=this.visibilityHandlers.get(t);i&&(document.removeEventListener("visibilitychange",i),this.visibilityHandlers.delete(t))}bumpMonitorGeneration(t){const i=(this.monitorGenerations.get(t)??0)+1;return this.monitorGenerations.set(t,i),i}getOperationGeneration(t){return this.operationGenerations.get(t)??0}bumpOperationGeneration(t){const i=this.getOperationGeneration(t)+1;return this.operationGenerations.set(t,i),i}canApplyAsyncResult(t,i,e,s){return!this.destroyed&&this.lifecycleGeneration===i&&((void 0===e||this.monitorGenerations.get(t)===e)&&(void 0===s||this.getOperationGeneration(t)===s))}enableAutoRebid(t,i,e){if(!a(i)||!a(e))throw s.validationError("Invalid monetary format. Must be numeric strings (e.g., '100.00')");if(d(i,"0")||d(e,"0"))throw s.validationError("Max bid and increment must be positive");if(r(e,i))throw s.validationError("Increment cannot be greater than max bid");const n=this.getTrackedParticipation(t);if(!n)throw s.validationError("Must enter auction before enabling auto-rebid");const o=u(n.currentBid),c=h(i,o);this.autoRebidConfigs.set(t,{enabled:!0,maxBid:i,increment:e,remainingBudget:c,rebidCount:0}),this.logger.info("Auto-rebid enabled",{auctionId:t,maxBid:i,increment:e,remainingBudget:c}),this.events.emit("auction:auto-rebid-enabled",{auctionId:t,maxBid:i,increment:e})}disableAutoRebid(t){const i=this.autoRebidConfigs.get(t);i&&(i.enabled=!1,this.logger.info("Auto-rebid disabled",{auctionId:t}),this.events.emit("auction:auto-rebid-disabled",{auctionId:t,finalRebidCount:i.rebidCount||0}))}getAutoRebidConfig(t){return this.autoRebidConfigs.get(t)}async handleAutoRebid(t,i){const e=this.autoRebidConfigs.get(t);if(e&&e.enabled)try{if(!a(i.highestBid)||!a(e.maxBid)||!a(e.increment))return void this.logger.error("Invalid auto-rebid amounts",{auctionId:t,config:e});const s=o(i.highestBid,e.increment);if(r(s,e.maxBid))return this.logger.info("Auto-rebid would exceed max bid, disabling",{auctionId:t,wouldBid:s,maxBid:e.maxBid}),this.disableAutoRebid(t),void this.events.emit("auction:auto-rebid-max-reached",{auctionId:t,maxBid:e.maxBid,wouldNeedToBid:s});this.logger.info("Placing auto-rebid",{auctionId:t,amount:s});const n=await this.bid(t,s);e.lastRebidAt=/* @__PURE__ */(new Date).toISOString(),e.rebidCount=(e.rebidCount||0)+1,e.remainingBudget=h(e.maxBid,s),this.events.emit("auction:auto-rebid-placed",{auctionId:t,amount:s,remainingBudget:e.remainingBudget,status:n.status})}catch(s){this.logger.error("Auto-rebid failed",{auctionId:t,error:s}),this.events.emit("auction:auto-rebid-failed",{auctionId:t,error:s})}}startMonitoring(t,i,e,s){this.monitorRuntime.start(t,i,e),this.trackedParticipations.has(t)||this.trackedParticipations.set(t,{auctionId:t,enteredAt:/* @__PURE__ */(new Date).toISOString(),status:"watching"}),this.startWatching(t,5e3,s)}stopMonitoring(t){this.stopWatching(t),this.monitorRuntime.stop(t)}isMonitoring(t){return this.monitorRuntime.has(t)||this.pollSchedulers.has(t)}destroy(){this.destroyed=!0,this.lifecycleGeneration+=1,this.stopAllWatching(),this.inFlightRequests.clear(),this.lastStatusCache.clear(),this.hasSuccessfulReadThisSession.clear(),this.lastSettleAtMs.clear(),this.visibilityHandlers.clear(),this.trackedParticipations.clear(),this.monitorGenerations.clear(),this.operationGenerations.clear(),this.bidHistoryCache.clear(),this.auctionDetailsCache.clear(),this.autoRebidConfigs.clear(),this.monitorRuntime.clear()}}export{b as AuctionManagementModule};
@@ -1,7 +1,7 @@
1
1
  import { AdmissionGrant, AuctionConsumerStatus, SequenceOutcome } from '@fanfare-io/fanfare-sdk-contracts/consumer-me';
2
2
  import { ReadableAtom } from 'nanostores';
3
3
  import { AuctionDisplayState } from '../experiences/distribution-monitor.types';
4
- import { DistributionSummary, WaitlistAttachment, WaitlistAttachmentView } from '../experiences/journey.types';
4
+ import { DistributionSummary } from '../experiences/journey.types';
5
5
  /**
6
6
  * Auction participation facts for one consumer.
7
7
  *
@@ -50,8 +50,6 @@ export interface AuctionScheduled {
50
50
  distribution: DistributionSummary & {
51
51
  type: "auction";
52
52
  };
53
- /** Scheduled-only waitlist attachment, cleared when the auction opens. */
54
- waitlist?: WaitlistAttachment;
55
53
  }
56
54
  /**
57
55
  * Bidding is open and this consumer has not bid yet.
@@ -154,8 +152,6 @@ export interface AuctionScheduledView {
154
152
  };
155
153
  /** ISO 8601 opening timestamp chosen from the distribution start/open time. */
156
154
  startsAt?: string;
157
- /** Scheduled-only waitlist actions; this attachment never grants access. */
158
- waitlist?: WaitlistAttachmentView;
159
155
  }
160
156
  /**
161
157
  * Enterable auction view rendered when the first bid can be placed.
@@ -184,7 +180,8 @@ export interface AuctionEnterableView {
184
180
  * Participating auction view rendered while the consumer is bidding.
185
181
  */
186
182
  export interface AuctionParticipatingView {
187
- /** Phase discriminator; `bid()` and `leave()` are legal on this view. */
183
+ /** Phase discriminator; `bid()` is legal on this view. Leave is not offered: a
184
+ * placed bid is a binding commitment (see auction.driver participating view). */
188
185
  phase: "participating";
189
186
  /** Mechanism discriminator for auction rendering. */
190
187
  mechanism: "auction";
@@ -200,13 +197,6 @@ export interface AuctionParticipatingView {
200
197
  * closed, or the server rejects the bid.
201
198
  */
202
199
  bid(amount: string): Promise<void>;
203
- /**
204
- * Leave the auction participation.
205
- *
206
- * Legal only while this view is current. The promise rejects if the consumer
207
- * is no longer participating or the server rejects the leave request.
208
- */
209
- leave(): Promise<void>;
210
200
  }
211
201
  /**
212
202
  * Settling auction view rendered while the server computes the auction result.
@@ -240,7 +230,7 @@ export interface AuctionGrantedView {
240
230
  /** Admission credential issued by the server for this auction win. */
241
231
  grant: AdmissionGrant;
242
232
  /**
243
- * Claim the auction admission credential for handoff.
233
+ * Claim the auction admission credential for checkout.
244
234
  *
245
235
  * Legal only while this view is current. It returns the credential
246
236
  * synchronously and may throw if the journey has already left the granted
@@ -1,2 +1,2 @@
1
1
  import{AppointmentManagementModule as e}from"../appointments/appointment.module.js";import{AuctionManagementModule as t}from"../auctions/auction.module.js";import{AuthenticationModule as r}from"../auth/auth.module.js";import{BeaconTrackingModule as n}from"../beacon/beacon.module.js";import{ChallengeManagementModule as i}from"../challenges/challenge.module.js";import{Config as o}from"../config/index.js";import{DrawManagementModule as s}from"../draws/draw.module.js";import{ExperienceManagementModule as a}from"../experiences/experience.module.js";import{ExperienceJourney as d}from"../experiences/journey.js";import{QueueManagementModule as l}from"../queues/queue.module.js";import{resolveCapabilityOptions as u}from"../state/capability-token-registry.js";import{getEventBus as c}from"../state/events.js";import{getSDKStore as g}from"../state/store.js";import{CrossTabCoordinator as y}from"../sync/cross-tab-coordinator.js";import{TimedReleaseManagementModule as m}from"../timed-releases/timed-release.module.js";import{version as f}from"../version.js";import{WaitlistManagementModule as p}from"../waitlists/waitlist.module.js";import{FanfareError as b,ErrorCodes as w}from"./errors.js";import{createHttpClient as h}from"./http.js";import{createLogger as v}from"./logger.js";let M=null;async function I(I){const S=new o(I),j=S.get(),A=v({enabled:j.debug||!1,level:j.debug?"debug":"warn",prefix:"Fanfare"});if(A.info("Initializing Fanfare SDK",{organizationId:I.organizationId,environment:j.environment}),M)throw new b("Fanfare SDK is already initialized. The SDK uses a shared event bus and store; multiple concurrent instances will corrupt state and analytics. Call destroy() on the existing instance before initializing a new one.",w.ALREADY_INITIALIZED);const k=/* @__PURE__ */Symbol("fanfare-sdk"),x=h({baseUrl:S.apiUrl,credentials:j.credentials,headers:{"X-Organization-Id":I.organizationId,"X-Publishable-Key":I.publishableKey,"X-Fanfare-API-Version":"2025-01-15","X-Fanfare-SDK-Version":f,"X-Fanfare-Client-Type":"browser"},timeout:3e4,retryConfig:{maxRetries:3,delayTimer:1e3,retryOnNetworkError:!0}},j),D=h({baseUrl:S.beaconUrl,credentials:"omit",headers:{"X-Organization-Id":I.organizationId,"X-Publishable-Key":I.publishableKey,"X-Fanfare-API-Version":"2025-01-15","X-Fanfare-SDK-Version":f,"X-Fanfare-Client-Type":"browser"},timeout:3e4,retryConfig:{maxRetries:3,delayTimer:1e3,retryOnNetworkError:!0}},j),E=new r(x,D),R=new i(x),q=new l(x),C=new s(x),z=new t(x),O=new e(x),F=new p(x),K=new m(x),X=new a(x,q,C,z,F,K),P=new n(D,j.beacon||{}),T=c(),J=g(),$=/* @__PURE__ */new Map,N=/* @__PURE__ */new Map,V=/* @__PURE__ */new Map;let B,U;const L=e=>{const t=N.get(e);if(t)return A.debug("Returning existing journey handle",{experienceId:e}),t;A.info("Creating new journey",{experienceId:e});const r=new d(e,B);$.set(e,r),U?.registerJourney(r);const n={view$:r.view$,events$:r.events$,latestEvent$:r.latestEvent$,snapshot$:r.state,ackEvent:e=>r.ackEvent(e),ackAllEvents:()=>r.ackAllEvents(),destroy:()=>{r.destroy(),U?.unregisterJourney(e),$.delete(e),N.delete(e)}};return N.set(e,n),n},G=async e=>{const t=e??await X.getMe(),r=/* @__PURE__ */new Set,n=new Set(t.active.journeys.map(e=>e.experienceId));for(const i of n)L(i);for(const i of n){const e=$.get(i);e&&await e.resumeFromMe(t)&&r.add(i)}return[...r]},H=async e=>{const t=/* @__PURE__ */new Set;for(const r of Object.keys(J.activeJourneys))L(r),t.add(r);if(!e&&!E.getSession())return[...t];for(const r of await G(e))t.add(r);return[...t]},_={check:()=>E.check(),guest:()=>E.guest(),requestOtp:e=>E.requestOtp(e),verifyOtp:e=>E.verifyOtp(e),exchangeExternal:e=>E.exchangeExternal(e),login:e=>E.login(e),logout:()=>E.logout(),getSession:()=>E.getSession(),refresh:()=>E.refresh()},Y={initiate:e=>R.initiate(e),verify:e=>R.verify(e)},Z={get:e=>L(e),list:()=>Array.from(
2
- /* @__PURE__ */new Set([...N.keys(),...$.keys(),...Object.keys(J.activeJourneys)])),resumeAll:e=>H(e)},Q={track:e=>P.track(e),trackBatch:e=>P.trackBatch(e),flush:()=>P.flush()},W=(e,t)=>{if(V.has(e.id))A.warn("CDP adapter already registered, skipping",{adapterId:e.id});else{try{e.init(T,t)}catch(r){return void A.error("CDP adapter failed to initialize",{adapterId:e.id,error:r})}V.set(e.id,e),A.info("CDP adapter registered",{adapterId:e.id})}},ee=(e,t)=>T.on(e,t),te=async()=>{A.info("Destroying SDK instance");for(const t of V.values())try{t.destroy()}catch(e){A.error("CDP adapter failed to destroy",{adapterId:t.id,error:e})}V.clear();for(const t of Array.from(N.values()))try{t.destroy()}catch(e){A.error("Journey handle failed to destroy",{error:e})}$.clear(),N.clear(),"destroy"in E&&"function"==typeof E.destroy&&E.destroy(),q.destroy(),C.destroy(),z.destroy(),O.destroy(),F.destroy(),K.destroy(),X.destroy(),P.destroy(),U?.close(),T.off(),M===k&&(M=null)};B={auth:_,challenges:Y,journeys:Z,queues:{get:e=>q.get(e),enter:(e,t,r)=>q.enter(e,t,u({distributionId:e},r)),leave:(e,t)=>q.leave(e,u({distributionId:e},t)),status:e=>q.status(e),startMonitoring:(e,t,r)=>q.startMonitoring(e,t,r),stopMonitoring:e=>q.stopMonitoring(e),isMonitoring:e=>q.isMonitoring(e)},draws:{get:e=>C.get(e),enter:(e,t,r)=>C.enter(e,t,u({distributionId:e},r)),leave:(e,t)=>C.leave(e,u({distributionId:e},t)),status:e=>C.status(e),startMonitoring:(e,t,r)=>C.startMonitoring(e,t,r),stopMonitoring:e=>C.stopMonitoring(e),isMonitoring:e=>C.isMonitoring(e)},auctions:{get:e=>z.get(e),bid:(e,t,r)=>z.bid(e,t,u({distributionId:e},r)),enter:(e,t,r)=>z.enter(e,t,u({distributionId:e},r)),leave:(e,t)=>z.leave(e,u({distributionId:e},t)),status:e=>z.status(e),getBidHistory:e=>z.getBidHistory(e),enableAutoRebid:(e,t,r)=>z.enableAutoRebid(e,t,r),disableAutoRebid:e=>z.disableAutoRebid(e),getAutoRebidConfig:e=>z.getAutoRebidConfig(e),startMonitoring:(e,t,r)=>z.startMonitoring(e,t,r),stopMonitoring:e=>z.stopMonitoring(e),isMonitoring:e=>z.isMonitoring(e),destroy:()=>z.destroy()},experiences:{get:e=>X.get(e),enter:e=>X.enter(e),leave:e=>X.leave(e),getMe:()=>X.getMe(),findSequence:(e,t)=>X.findSequence(e,t),validateSequenceAccess:(e,t)=>X.validateSequenceAccess(e,t),selectSequence:e=>X.selectSequence(e),getCurrentDistributions:e=>X.getCurrentDistributions(e),enterDistribution:e=>{if(!e)throw new b("Distribution is required",w.VALIDATION_ERROR);return X.enterDistribution(e)},getActiveSession:()=>X.getActiveSession(),isInExperience:e=>X.isInExperience(e),getSelectedSequence:()=>X.getSelectedSequence(),createJourney:e=>L(e),resumeJourneysFromMe:e=>G(e),destroy:()=>X.destroy()},waitlists:{enter:(e,t)=>F.enter(e,u({distributionId:e},t)),leave:(e,t)=>F.leave(e,u({distributionId:e},t)),getStatus:e=>F.getStatus(e),destroy:()=>F.destroy()},timedReleases:{get:e=>K.get(e),enter:(e,t,r)=>K.enter(e,t,u({distributionId:e},r)),leave:(e,t)=>K.leave(e,u({distributionId:e},t)),complete:e=>K.complete(e),status:e=>K.status(e),startMonitoring:(e,t,r)=>K.startMonitoring(e,t,r),stopMonitoring:e=>K.stopMonitoring(e),isMonitoring:e=>K.isMonitoring(e)},appointments:{get:e=>O.get(e),getSlots:(e,t)=>O.getSlots(e,t),getMe:e=>O.getMe(e),book:(e,t,r)=>O.book(e,t,r),cancel:(e,t,r,n)=>O.cancel(e,t,r,n),reschedule:(e,t,r)=>O.reschedule(e,t,r),startMonitoring:(e,t,r)=>O.startMonitoring(e,t,r),stopMonitoring:e=>O.stopMonitoring(e),isMonitoring:e=>O.isMonitoring(e),bumpMonitorGeneration:e=>O.bumpMonitorGeneration(e),destroy:()=>O.destroy()},beacon:Q,use:W,on:ee,destroy:te};const re=!1!==I.sync&&"object"==typeof I.sync?I.sync:{};U=new y({enabled:!1!==I.sync&&!1!==re.enabled,channelName:re.channelName}),A.info("Inter-tab sync initialized",{tabId:U.getTabId(),enabled:U.isEnabled()});const ne={auth:_,challenges:Y,journeys:Z,beacon:Q,appointments:B.appointments,use:W,on:ee,destroy:te};if(await E.rehydrate(),j.autoResume&&E.getSession())try{await H()}catch(ie){A.warn("Auto-resume on init failed; journeys can be resumed manually",{error:ie instanceof Error?ie.message:String(ie)})}return M=k,A.info("SDK initialized successfully"),ne}export{I as init};
2
+ /* @__PURE__ */new Set([...N.keys(),...$.keys(),...Object.keys(J.activeJourneys)])),resumeAll:e=>H(e)},Q={track:e=>P.track(e),trackBatch:e=>P.trackBatch(e),flush:()=>P.flush()},W=(e,t)=>{if(V.has(e.id))A.warn("CDP adapter already registered, skipping",{adapterId:e.id});else{try{e.init(T,t)}catch(r){return void A.error("CDP adapter failed to initialize",{adapterId:e.id,error:r})}V.set(e.id,e),A.info("CDP adapter registered",{adapterId:e.id})}},ee=(e,t)=>T.on(e,t),te=async()=>{A.info("Destroying SDK instance");for(const t of V.values())try{t.destroy()}catch(e){A.error("CDP adapter failed to destroy",{adapterId:t.id,error:e})}V.clear();for(const t of Array.from(N.values()))try{t.destroy()}catch(e){A.error("Journey handle failed to destroy",{error:e})}$.clear(),N.clear(),"destroy"in E&&"function"==typeof E.destroy&&E.destroy(),q.destroy(),C.destroy(),z.destroy(),O.destroy(),F.destroy(),K.destroy(),X.destroy(),P.destroy(),U?.close(),T.off(),M===k&&(M=null)};B={auth:_,challenges:Y,journeys:Z,queues:{get:e=>q.get(e),enter:(e,t,r)=>q.enter(e,t,u({distributionId:e},r)),leave:(e,t)=>q.leave(e,u({distributionId:e},t)),status:e=>q.status(e),startMonitoring:(e,t,r)=>q.startMonitoring(e,t,r),stopMonitoring:e=>q.stopMonitoring(e),isMonitoring:e=>q.isMonitoring(e)},draws:{get:e=>C.get(e),enter:(e,t,r)=>C.enter(e,t,u({distributionId:e},r)),leave:(e,t)=>C.leave(e,u({distributionId:e},t)),status:e=>C.status(e),startMonitoring:(e,t,r)=>C.startMonitoring(e,t,r),stopMonitoring:e=>C.stopMonitoring(e),isMonitoring:e=>C.isMonitoring(e)},auctions:{get:e=>z.get(e),bid:(e,t,r)=>z.bid(e,t,u({distributionId:e},r)),enter:(e,t,r)=>z.enter(e,t,u({distributionId:e},r)),leave:(e,t)=>z.leave(e,u({distributionId:e},t)),status:e=>z.status(e),getBidHistory:e=>z.getBidHistory(e),enableAutoRebid:(e,t,r)=>z.enableAutoRebid(e,t,r),disableAutoRebid:e=>z.disableAutoRebid(e),getAutoRebidConfig:e=>z.getAutoRebidConfig(e),startMonitoring:(e,t,r)=>z.startMonitoring(e,t,r),stopMonitoring:e=>z.stopMonitoring(e),isMonitoring:e=>z.isMonitoring(e),destroy:()=>z.destroy()},experiences:{get:e=>X.get(e),enter:e=>X.enter(e),leave:e=>X.leave(e),getMe:()=>X.getMe(),findSequence:(e,t)=>X.findSequence(e,t),validateSequenceAccess:(e,t)=>X.validateSequenceAccess(e,t),selectSequence:e=>X.selectSequence(e),getCurrentDistributions:e=>X.getCurrentDistributions(e),enterDistribution:e=>{if(!e)throw new b("Distribution is required",w.VALIDATION_ERROR);return X.enterDistribution(e)},getActiveSession:()=>X.getActiveSession(),isInExperience:e=>X.isInExperience(e),getSelectedSequence:()=>X.getSelectedSequence(),createJourney:e=>L(e),resumeJourneysFromMe:e=>G(e),destroy:()=>X.destroy()},waitlists:{enter:(e,t)=>F.enter(e,u({waitlistId:e},t)),leave:(e,t)=>F.leave(e,u({waitlistId:e},t)),getStatus:e=>F.getStatus(e),destroy:()=>F.destroy()},timedReleases:{get:e=>K.get(e),enter:(e,t,r)=>K.enter(e,t,u({distributionId:e},r)),leave:(e,t)=>K.leave(e,u({distributionId:e},t)),complete:e=>K.complete(e),status:e=>K.status(e),startMonitoring:(e,t,r)=>K.startMonitoring(e,t,r),stopMonitoring:e=>K.stopMonitoring(e),isMonitoring:e=>K.isMonitoring(e)},appointments:{get:e=>O.get(e),getSlots:(e,t)=>O.getSlots(e,t),getMe:e=>O.getMe(e),book:(e,t,r)=>O.book(e,t,r),cancel:(e,t,r,n)=>O.cancel(e,t,r,n),reschedule:(e,t,r)=>O.reschedule(e,t,r),startMonitoring:(e,t,r)=>O.startMonitoring(e,t,r),stopMonitoring:e=>O.stopMonitoring(e),isMonitoring:e=>O.isMonitoring(e),bumpMonitorGeneration:e=>O.bumpMonitorGeneration(e),destroy:()=>O.destroy()},beacon:Q,use:W,on:ee,destroy:te};const re=!1!==I.sync&&"object"==typeof I.sync?I.sync:{};U=new y({enabled:!1!==I.sync&&!1!==re.enabled,channelName:re.channelName}),A.info("Inter-tab sync initialized",{tabId:U.getTabId(),enabled:U.isEnabled()});const ne={auth:_,challenges:Y,journeys:Z,beacon:Q,appointments:B.appointments,use:W,on:ee,destroy:te};if(await E.rehydrate(),j.autoResume&&E.getSession())try{await H()}catch(ie){A.warn("Auto-resume on init failed; journeys can be resumed manually",{error:ie instanceof Error?ie.message:String(ie)})}return M=k,A.info("SDK initialized successfully"),ne}export{I as init};
@@ -1 +1 @@
1
- import{classify as r}from"./error-disposition.js";import{FanfareError as e,ErrorCodes as t}from"./errors.js";const o={[t.OTP_INVALID]:"auth.invalidCode",[t.OTP_EXPIRED]:"auth.codeExpired",[t.INVALID_SESSION]:"common.sessionExpired",[t.INVALID_REFRESH_TOKEN]:"common.sessionExpired"};function i(r){return"INLINE"===r||"STEP_RESET"===r?"error.inlineGeneric":"error.description"}function s(e,s){const{disposition:n}=r(e.code,e.status,e.action),d="number"==typeof e.retryAfter&&Number.isFinite(e.retryAfter)?e.retryAfter:void 0,c=function(r,e,s){return r===t.RATE_LIMITED?s?"error.rateLimited":"error.rateLimitedGeneric":o[r]??i(e)}(e.code,n,void 0!==d);let u=s(c,void 0!==d?{values:{seconds:d}}:void 0);u.includes("{{")&&(u=s(i(n)));const E="PANEL"===n?s("error.title"):void 0;return{code:e.code,title:E,description:u,disposition:n,retryAfter:e.retryAfter,support:e.support}}function n(r){if(r instanceof e)return r;const o=r instanceof Error?r.message:String(r);return new e(o,t.INTERNAL_ERROR)}function d(r){return{code:r.code,status:r.status,requestId:r.requestId,correlationId:r.correlationId,details:r.details,issues:r.issues}}export{n as ensureFanfareError,d as toDebug,s as toDisplay};
1
+ import{classify as r}from"./error-disposition.js";import{FanfareError as e,ErrorCodes as o}from"./errors.js";const t={[o.OTP_INVALID]:"auth.invalidCode",[o.OTP_EXPIRED]:"auth.codeExpired",[o.INVALID_SESSION]:"common.sessionExpired",[o.INVALID_REFRESH_TOKEN]:"common.sessionExpired",[o.SERVICE_UNAVAILABLE]:"error.overloadedGeneric",[o.TIMEOUT]:"error.overloadedGeneric",[o.NETWORK_ERROR]:"error.overloadedGeneric",[o.FEATURE_DISABLED]:"error.featureDisabled"};function i(r){return"INLINE"===r||"STEP_RESET"===r?"error.inlineGeneric":"error.description"}function n(e,n){const{disposition:s}=r(e.code,e.status,e.action),d="number"==typeof e.retryAfter&&Number.isFinite(e.retryAfter)?e.retryAfter:void 0,c=function(r,e,n){return r===o.RATE_LIMITED?n?"error.rateLimited":"error.rateLimitedGeneric":r===o.OVERLOADED?n?"error.overloaded":"error.overloadedGeneric":t[r]??i(e)}(e.code,s,void 0!==d);let E=n(c,void 0!==d?{values:{seconds:d}}:void 0);E.includes("{{")&&(E=n(i(s)));const u="PANEL"===s?n("error.title"):void 0;return{code:e.code,title:u,description:E,disposition:s,retryAfter:e.retryAfter,support:e.support}}function s(r){if(r instanceof e)return r;const t=r instanceof Error?r.message:String(r);return new e(t,o.INTERNAL_ERROR)}function d(r){return{code:r.code,status:r.status,requestId:r.requestId,correlationId:r.correlationId,details:r.details,issues:r.issues}}export{s as ensureFanfareError,d as toDebug,n as toDisplay};
@@ -1 +1 @@
1
- import{ErrorCodes as E}from"./errors.js";const I={[E.OTP_INVALID]:"INLINE",[E.INVALID_CREDENTIALS]:"INLINE",[E.VALIDATION_ERROR]:"INLINE",[E.RATE_LIMITED]:"INLINE",[E.SLOT_OUT_OF_WINDOW]:"INLINE",[E.OTP_EXPIRED]:"STEP_RESET",[E.BOT_CHALLENGE_INVALID]:"STEP_RESET",[E.BOT_CHALLENGE_EXPIRED]:"STEP_RESET",[E.SLOT_EXPIRED]:"STEP_RESET",[E.SLOT_NOT_AVAILABLE]:"STEP_RESET",[E.INVALID_SESSION]:"RESTART",[E.INVALID_REFRESH_TOKEN]:"RESTART",[E.UNAUTHORIZED]:"RESTART",[E.ENTRY_TOKEN_REQUIRED]:"RESTART",[E.ENTRY_TOKEN_INVALID]:"RESTART",[E.ENTRY_TOKEN_EXPIRED]:"RESTART",[E.ADMISSION_GRANT_INVALID]:"RESTART",[E.ADMISSION_PROOF_REQUIRED]:"RESTART",[E.ADMISSION_KEY_THUMBPRINT_REQUIRED]:"RESTART",[E.ADMISSION_ORIGINAL_ENTRY_REQUIRED]:"RESTART",[E.RATE_LIMIT_UNAVAILABLE]:"PANEL",[E.FINGERPRINT_REQUIRED]:"PANEL",[E.FINGERPRINT_DEVICE_MISMATCH]:"PANEL",[E.FINGERPRINT_INVALID]:"PANEL",[E.BOT_CHALLENGE_SUBJECT_MISMATCH]:"PANEL",[E.ADMISSION_KEY_MISMATCH]:"PANEL",[E.DISTRIBUTION_NOT_OPEN]:"PANEL",[E.DISTRIBUTION_CLOSED]:"PANEL",[E.DISTRIBUTION_FULL]:"PANEL",[E.DISTRIBUTION_ORDER_LIMIT]:"PANEL",[E.PROTOCOL_ERROR]:"PANEL"},T=/* @__PURE__ */new Set([E.ENTRY_TOKEN_MISMATCH]);function R(E,R,N){const _=function(E,R){return"reroute"===R&&T.has(E)?"RESTART":I[E]??"PANEL"}(E,N);return{disposition:_,autoRetryOnce:false}}export{R as classify};
1
+ import{ErrorCodes as E}from"./errors.js";const I={[E.OTP_INVALID]:"INLINE",[E.INVALID_CREDENTIALS]:"INLINE",[E.VALIDATION_ERROR]:"INLINE",[E.RATE_LIMITED]:"INLINE",[E.SLOT_OUT_OF_WINDOW]:"INLINE",[E.OVERLOADED]:"INLINE",[E.SERVICE_UNAVAILABLE]:"INLINE",[E.TIMEOUT]:"INLINE",[E.NETWORK_ERROR]:"INLINE",[E.FEATURE_DISABLED]:"INLINE",[E.OTP_EXPIRED]:"STEP_RESET",[E.BOT_CHALLENGE_INVALID]:"STEP_RESET",[E.BOT_CHALLENGE_EXPIRED]:"STEP_RESET",[E.SLOT_EXPIRED]:"STEP_RESET",[E.SLOT_NOT_AVAILABLE]:"STEP_RESET",[E.INVALID_SESSION]:"RESTART",[E.INVALID_REFRESH_TOKEN]:"RESTART",[E.UNAUTHORIZED]:"RESTART",[E.ENTRY_TOKEN_REQUIRED]:"RESTART",[E.ENTRY_TOKEN_INVALID]:"RESTART",[E.ENTRY_TOKEN_EXPIRED]:"RESTART",[E.ADMISSION_GRANT_INVALID]:"RESTART",[E.ADMISSION_PROOF_REQUIRED]:"RESTART",[E.ADMISSION_KEY_THUMBPRINT_REQUIRED]:"RESTART",[E.ADMISSION_ORIGINAL_ENTRY_REQUIRED]:"RESTART",[E.RATE_LIMIT_UNAVAILABLE]:"PANEL",[E.FINGERPRINT_REQUIRED]:"PANEL",[E.FINGERPRINT_DEVICE_MISMATCH]:"PANEL",[E.FINGERPRINT_INVALID]:"PANEL",[E.BOT_CHALLENGE_SUBJECT_MISMATCH]:"PANEL",[E.ADMISSION_KEY_MISMATCH]:"PANEL",[E.DISTRIBUTION_NOT_OPEN]:"PANEL",[E.DISTRIBUTION_CLOSED]:"PANEL",[E.DISTRIBUTION_FULL]:"PANEL",[E.DISTRIBUTION_ORDER_LIMIT]:"PANEL",[E.PROTOCOL_ERROR]:"PANEL"},T=/* @__PURE__ */new Set([E.ENTRY_TOKEN_MISMATCH]);function R(E,R,N){const A=function(E,R){return"reroute"===R&&T.has(E)?"RESTART":I[E]??"PANEL"}(E,N);return{disposition:A,autoRetryOnce:false}}export{R as classify};
@@ -14,6 +14,7 @@ export declare const SdkClientErrorCodes: {
14
14
  readonly INVALID_CONFIG: "INVALID_CONFIG";
15
15
  readonly ADMISSION_PROOF_UNAVAILABLE: "ADMISSION_PROOF_UNAVAILABLE";
16
16
  readonly PROTOCOL_ERROR: "PROTOCOL_ERROR";
17
+ readonly CANNOT_LEAVE_AFTER_BID: "CANNOT_LEAVE_AFTER_BID";
17
18
  };
18
19
  export type SdkClientErrorCode = (typeof SdkClientErrorCodes)[keyof typeof SdkClientErrorCodes];
19
20
  /**
@@ -30,6 +31,9 @@ export declare const ErrorCodes: {
30
31
  readonly INVALID_CONFIG: "INVALID_CONFIG";
31
32
  readonly ADMISSION_PROOF_UNAVAILABLE: "ADMISSION_PROOF_UNAVAILABLE";
32
33
  readonly PROTOCOL_ERROR: "PROTOCOL_ERROR";
34
+ readonly CANNOT_LEAVE_AFTER_BID: "CANNOT_LEAVE_AFTER_BID";
35
+ readonly OVERLOADED: "OVERLOADED";
36
+ readonly FEATURE_DISABLED: "FEATURE_DISABLED";
33
37
  readonly BOT_CHALLENGE_INVALID: "BOT_CHALLENGE_INVALID";
34
38
  readonly BOT_CHALLENGE_EXPIRED: "BOT_CHALLENGE_EXPIRED";
35
39
  readonly BOT_CHALLENGE_SUBJECT_MISMATCH: "BOT_CHALLENGE_SUBJECT_MISMATCH";
@@ -79,6 +83,8 @@ export declare const ErrorCodes: {
79
83
  */
80
84
  export type KnownErrorCode = ContractErrorCode | SdkClientErrorCode;
81
85
  export type ErrorCode = KnownErrorCode | (string & {});
86
+ /** Parses an RFC 7231 Retry-After header (delta-seconds or HTTP-date) into whole seconds, or undefined. */
87
+ export declare function parseRetryAfterSeconds(header: string | null): number | undefined;
82
88
  /**
83
89
  * Base error class for the Fanfare SDK.
84
90
  */
@@ -103,10 +109,13 @@ export declare class FanfareError extends Error {
103
109
  readonly action?: string;
104
110
  /** Support metadata for surfacing reference/confirmation codes. */
105
111
  readonly support?: ConsumerApiError["support"];
112
+ /** Host adapters may render an already-held snapshot quietly while still reporting this error. */
113
+ readonly quietable?: boolean;
106
114
  constructor(message: string, code: KnownErrorCode | (string & {}), status?: number, details?: unknown, requestId?: string, correlationId?: string, retryable?: boolean, retryAfter?: number, extra?: {
107
115
  issues?: readonly unknown[];
108
116
  action?: string;
109
117
  support?: ConsumerApiError["support"];
118
+ quietable?: boolean;
110
119
  });
111
120
  /**
112
121
  * Normalize an HTTP error response into a `FanfareError`. Preserves
@@ -173,4 +182,5 @@ export declare const createError: {
173
182
  invalidConfig: (message: string, details?: unknown) => FanfareError;
174
183
  validationError: (message: string, details?: unknown) => FanfareError;
175
184
  internalError: (message: string, details?: unknown) => FanfareError;
185
+ cannotLeaveAfterBid: (auctionId: string) => FanfareError;
176
186
  };
@@ -1 +1 @@
1
- import{ErrorCodeMap as I}from"@fanfare-io/fanfare-sdk-contracts/errors";const e={NETWORK_ERROR:"NETWORK_ERROR",TIMEOUT:"TIMEOUT",ABORTED:"ABORTED",NOT_INITIALIZED:"NOT_INITIALIZED",ALREADY_INITIALIZED:"ALREADY_INITIALIZED",INVALID_CONFIG:"INVALID_CONFIG",ADMISSION_PROOF_UNAVAILABLE:"ADMISSION_PROOF_UNAVAILABLE",PROTOCOL_ERROR:"PROTOCOL_ERROR"},t={...I,...e};function r(I,e){if(I&&"object"==typeof I&&e in I){const t=I[e];if("string"==typeof t)return t}}const E=/* @__PURE__ */new Set([I.VALIDATION_ERROR,I.UNAUTHORIZED,I.FORBIDDEN,I.NOT_FOUND,I.RATE_LIMITED,I.RATE_LIMIT_UNAVAILABLE,I.PAYLOAD_TOO_LARGE,I.INVALID_CREDENTIALS,I.INVALID_SESSION,I.INVALID_REFRESH_TOKEN,I.OTP_INVALID,I.OTP_EXPIRED,I.ENTRY_TOKEN_REQUIRED,I.ENTRY_TOKEN_INVALID,I.ENTRY_TOKEN_EXPIRED,I.ENTRY_TOKEN_MISMATCH,I.SLOT_EXPIRED,I.SLOT_OUT_OF_WINDOW,I.ALREADY_BOOKED,I.ACTIVE_BOOKING_EXISTS,I.NO_EXISTING_BOOKING,I.INVALID_SLOT,I.CANNOT_CANCEL_ONGOING_OR_PAST,I.FINGERPRINT_REQUIRED,I.FINGERPRINT_DEVICE_MISMATCH,I.FINGERPRINT_INVALID,I.SLOT_NOT_AVAILABLE,I.NEW_SLOT_FULL,I.DISTRIBUTION_NOT_OPEN,I.DISTRIBUTION_CLOSED,I.DISTRIBUTION_FULL,I.ADMISSION_KEY_THUMBPRINT_REQUIRED,I.ADMISSION_PROOF_REQUIRED,I.ADMISSION_GRANT_INVALID,I.ADMISSION_ORIGINAL_ENTRY_REQUIRED,I.ADMISSION_KEY_MISMATCH,I.BOT_CHALLENGE_INVALID,I.BOT_CHALLENGE_EXPIRED,I.BOT_CHALLENGE_SUBJECT_MISMATCH]);class O extends Error{constructor(I,e,t,r,E,N,R=!1,o,_){super(I),this.name="FanfareError",this.code=e,this.status=t,this.details=r,this.requestId=E,this.correlationId=N,this.retryable=R,this.retryAfter=o,this.issues=_?.issues,this.action=_?.action,this.support=_?.support,Error.captureStackTrace&&Error.captureStackTrace(this,O)}static fromResponse(I,e,t){const R=I.headers.get("X-Request-Id")||void 0,o=I.headers.get("X-Correlation-Id")||t,_=I.headers.get("Retry-After"),T=_?Number(_):void 0,{code:s,retryable:n}=N(I.status,e),A=r(e,"message"),i=(A&&I.status<500&&E.has(s)?A:null)||I.statusText||"An error occurred",a=(e&&"object"==typeof e&&"details"in e?e.details:e)??e,D=e&&"object"==typeof e&&Array.isArray(e.issues)?e.issues:void 0,L=r(e,"action"),c=e&&"object"==typeof e&&"support"in e?e.support:void 0;return new O(i,s,I.status,a,R,o,n,T,{issues:D,action:L,support:c})}is(I){return this.code===I}}function N(I,e){const E=429===I||I>=500,O=r(e,"code");return O?{code:O,retryable:E}:429===I?{code:t.RATE_LIMITED,retryable:E}:401===I?{code:t.UNAUTHORIZED,retryable:E}:403===I?{code:t.FORBIDDEN,retryable:E}:413===I?{code:t.PAYLOAD_TOO_LARGE,retryable:E}:422===I||400===I?{code:t.VALIDATION_ERROR,retryable:E}:404===I?{code:t.NOT_FOUND,retryable:E}:503===I?{code:t.SERVICE_UNAVAILABLE,retryable:E}:I>=500?{code:t.INTERNAL_ERROR,retryable:E}:{code:`HTTP_${I}`,retryable:E}}class R extends O{constructor(I,e,r,E){super(I,t.PROTOCOL_ERROR,void 0,r,E),this.name="ProtocolError",this.endpoint=e,this.issues=r}}function o(I){return I instanceof O}function _(I){return I instanceof R}const T=/* @__PURE__ */new Set([t.ENTRY_TOKEN_REQUIRED,t.ENTRY_TOKEN_INVALID,t.ENTRY_TOKEN_EXPIRED,t.ENTRY_TOKEN_MISMATCH]);function s(I){return I instanceof O&&(403===I.status&&T.has(I.code))}function n(I){return s(I)}function A(I){return!!s(I)&&"reroute"===I.action}function i(I){return A(I)}const a={networkError:(I,e)=>new O(I,t.NETWORK_ERROR,void 0,e),timeout:(I="Request timed out")=>new O(I,t.TIMEOUT),unauthorized:(I="Unauthorized")=>new O(I,t.UNAUTHORIZED,401),invalidSession:(I="Session is no longer valid")=>new O(I,t.INVALID_SESSION,401),notInitialized:(I="SDK not initialized")=>new O(I,t.NOT_INITIALIZED),invalidConfig:(I,e)=>new O(I,t.INVALID_CONFIG,void 0,e),validationError:(I,e)=>new O(I,t.VALIDATION_ERROR,void 0,e),internalError:(I,e)=>new O(I,t.INTERNAL_ERROR,void 0,e)};export{t as ErrorCodes,O as FanfareError,R as ProtocolError,e as SdkClientErrorCodes,N as classifyHttpError,a as createError,n as isCapabilityGrantError,i as isCapabilityGrantRerouteError,s as isCapabilityTokenError,A as isCapabilityTokenRerouteError,o as isFanfareError,_ as isProtocolError};
1
+ import{ErrorCodeMap as e}from"@fanfare-io/fanfare-sdk-contracts/errors";const t={NETWORK_ERROR:"NETWORK_ERROR",TIMEOUT:"TIMEOUT",ABORTED:"ABORTED",NOT_INITIALIZED:"NOT_INITIALIZED",ALREADY_INITIALIZED:"ALREADY_INITIALIZED",INVALID_CONFIG:"INVALID_CONFIG",ADMISSION_PROOF_UNAVAILABLE:"ADMISSION_PROOF_UNAVAILABLE",PROTOCOL_ERROR:"PROTOCOL_ERROR",CANNOT_LEAVE_AFTER_BID:"CANNOT_LEAVE_AFTER_BID"},I={...e,...t};function E(e,t){if(e&&"object"==typeof e&&t in e){const I=e[t];if("string"==typeof I)return I}}function r(e){if(!e)return;const t=+e,I=Math.ceil(isFinite(t)?t:(Date.parse(e)-Date.now())/1e3);return I>=0?I:I<0?0:void 0}const o=/* @__PURE__ */new Set([e.VALIDATION_ERROR,e.UNAUTHORIZED,e.FORBIDDEN,e.NOT_FOUND,e.RATE_LIMITED,e.RATE_LIMIT_UNAVAILABLE,e.PAYLOAD_TOO_LARGE,e.INVALID_CREDENTIALS,e.INVALID_SESSION,e.INVALID_REFRESH_TOKEN,e.OTP_INVALID,e.OTP_EXPIRED,e.ENTRY_TOKEN_REQUIRED,e.ENTRY_TOKEN_INVALID,e.ENTRY_TOKEN_EXPIRED,e.ENTRY_TOKEN_MISMATCH,e.SLOT_EXPIRED,e.SLOT_OUT_OF_WINDOW,e.ALREADY_BOOKED,e.ACTIVE_BOOKING_EXISTS,e.NO_EXISTING_BOOKING,e.INVALID_SLOT,e.CANNOT_CANCEL_ONGOING_OR_PAST,e.FINGERPRINT_REQUIRED,e.FINGERPRINT_DEVICE_MISMATCH,e.FINGERPRINT_INVALID,e.SLOT_NOT_AVAILABLE,e.NEW_SLOT_FULL,e.DISTRIBUTION_NOT_OPEN,e.DISTRIBUTION_CLOSED,e.DISTRIBUTION_FULL,e.ADMISSION_KEY_THUMBPRINT_REQUIRED,e.ADMISSION_PROOF_REQUIRED,e.ADMISSION_GRANT_INVALID,e.ADMISSION_ORIGINAL_ENTRY_REQUIRED,e.ADMISSION_KEY_MISMATCH,e.BOT_CHALLENGE_INVALID,e.BOT_CHALLENGE_EXPIRED,e.BOT_CHALLENGE_SUBJECT_MISMATCH]);class _ extends Error{constructor(e,t,I,E,r,o,n=!1,N,O){super(e),this.name="FanfareError",this.code=t,this.status=I,this.details=E,this.requestId=r,this.correlationId=o,this.retryable=n,this.retryAfter=N,this.issues=O?.issues,this.action=O?.action,this.support=O?.support,this.quietable=O?.quietable,Error.captureStackTrace&&Error.captureStackTrace(this,_)}static fromResponse(e,t,I){const N=e.headers.get("X-Request-Id")||void 0,O=e.headers.get("X-Correlation-Id")||I,R=r(e.headers.get("Retry-After")),{code:i,retryable:T}=n(e.status,t),s=E(t,"message"),A=(s&&e.status<500&&o.has(i)?s:null)||e.statusText||"An error occurred",a=(t&&"object"==typeof t&&"details"in t?t.details:t)??t,D=t&&"object"==typeof t&&Array.isArray(t.issues)?t.issues:void 0,L=E(t,"action"),c=t&&"object"==typeof t&&"support"in t?t.support:void 0;return new _(A,i,e.status,a,N,O,T,R,{issues:D,action:L,support:c})}is(e){return this.code===e}}function n(e,t){const r=429===e||e>=500,o=E(t,"code");return o?{code:o,retryable:r}:429===e?{code:I.RATE_LIMITED,retryable:r}:401===e?{code:I.UNAUTHORIZED,retryable:r}:403===e?{code:I.FORBIDDEN,retryable:r}:413===e?{code:I.PAYLOAD_TOO_LARGE,retryable:r}:422===e||400===e?{code:I.VALIDATION_ERROR,retryable:r}:404===e?{code:I.NOT_FOUND,retryable:r}:503===e?{code:I.SERVICE_UNAVAILABLE,retryable:r}:e>=500?{code:I.INTERNAL_ERROR,retryable:r}:{code:`HTTP_${e}`,retryable:r}}class N extends _{constructor(e,t,E,r){super(e,I.PROTOCOL_ERROR,void 0,E,r),this.name="ProtocolError",this.endpoint=t,this.issues=E}}function O(e){return e instanceof _}function R(e){return e instanceof N}const i=/* @__PURE__ */new Set([I.ENTRY_TOKEN_REQUIRED,I.ENTRY_TOKEN_INVALID,I.ENTRY_TOKEN_EXPIRED,I.ENTRY_TOKEN_MISMATCH]);function T(e){return e instanceof _&&(403===e.status&&i.has(e.code))}function s(e){return T(e)}function A(e){return!!T(e)&&"reroute"===e.action}function a(e){return A(e)}const D={networkError:(e,t)=>new _(e,I.NETWORK_ERROR,void 0,t),timeout:(e="Request timed out")=>new _(e,I.TIMEOUT),unauthorized:(e="Unauthorized")=>new _(e,I.UNAUTHORIZED,401),invalidSession:(e="Session is no longer valid")=>new _(e,I.INVALID_SESSION,401),notInitialized:(e="SDK not initialized")=>new _(e,I.NOT_INITIALIZED),invalidConfig:(e,t)=>new _(e,I.INVALID_CONFIG,void 0,t),validationError:(e,t)=>new _(e,I.VALIDATION_ERROR,void 0,t),internalError:(e,t)=>new _(e,I.INTERNAL_ERROR,void 0,t),cannotLeaveAfterBid:e=>new _(`Cannot leave auction ${e} after placing a bid — a bid is a binding commitment`,I.CANNOT_LEAVE_AFTER_BID,void 0,{auctionId:e})};export{I as ErrorCodes,_ as FanfareError,N as ProtocolError,t as SdkClientErrorCodes,n as classifyHttpError,D as createError,s as isCapabilityGrantError,a as isCapabilityGrantRerouteError,T as isCapabilityTokenError,A as isCapabilityTokenRerouteError,O as isFanfareError,R as isProtocolError,r as parseRetryAfterSeconds};
@@ -41,6 +41,7 @@ export declare class DefaultHttpClient implements HttpClient {
41
41
  private clientSessionId;
42
42
  private fingerprint;
43
43
  private fingerprintingEnabled;
44
+ private readonly inFlightGetRequests;
44
45
  constructor(config: HttpClientConfig, sdkConfig?: FanfareConfig);
45
46
  /**
46
47
  * Initialize browser fingerprint asynchronously
package/dist/core/http.js CHANGED
@@ -1 +1 @@
1
- import e from"wretch";import{retry as t}from"wretch/middlewares";import{buildAdmissionProofHeaders as r,getAdmissionKeyThumbprint as i}from"../security/admission-proof.js";import{generateFingerprint as s}from"../utils/fingerprint.module.js";import{FanfareError as o,ErrorCodes as n}from"./errors.js";import{getLogger as a}from"./logger.js";import{isStorageAvailable as c,generateId as l}from"./utils.js";class g{constructor(e,t){this.logger=a(),this.fingerprint=null,this.config=e,this.fingerprintingEnabled=!1!==t?.features?.fingerprinting;const r="fanfare_client_session_id";let i=null;if(c("localStorage"))try{i=window.localStorage.getItem(r)}catch{i=null}if(this.clientSessionId=i||l(),!i&&c("localStorage"))try{window.localStorage.setItem(r,this.clientSessionId)}catch{}this.fingerprintingEnabled?this.initializeFingerprint():this.logger.debug("Browser fingerprinting disabled by configuration")}async initializeFingerprint(){try{const e=await s();this.fingerprint=e.hash,this.logger.debug("Browser fingerprint generated successfully",{fingerprintHash:this.fingerprint})}catch(e){this.logger.debug("Failed to generate browser fingerprint",{error:e instanceof Error?e.message:"Unknown error"}),this.fingerprint=null}}getClient(i){const{retryConfig:s={}}=this.config,o={"Content-Type":"application/json","X-Client-Session-Id":this.clientSessionId,...this.config.headers};return this.fingerprint&&(o["X-Fingerprint"]=this.fingerprint),e(this.config.baseUrl).polyfills({fetch:(e,t)=>globalThis.fetch(e,t)}).options({credentials:this.config.credentials??"include",mode:"cors"}).headers(o).middlewares([t({delayTimer:s.delayTimer||1e3,maxAttempts:s.maxRetries||3,until:(e,t)=>e?!!e.ok||e.status>=400&&e.status<500&&429!==e.status:(t&&s.retryOnNetworkError,!1),onRetry:async e=>{this.logger.debug("Retrying request",{status:e.response?.status,error:e.error?.message,retryAfter:e.response?.headers?.get("Retry-After")??void 0});const t=e.options.headers&&"object"==typeof e.options.headers&&!Array.isArray(e.options.headers)?e.options.headers:{};if(!Object.keys(t).some(e=>"dpop"===e.toLowerCase()))return;const i="string"==typeof e.options.method?e.options.method:"POST",s=this.config.baseUrl,o=e.url.startsWith(s)?new URL(e.url.slice(s.length),s).toString():e.url,n=await r({url:o,method:i});return n?{options:{...e.options,headers:{...t,...n}}}:void 0}})]).catcher(401,async e=>{throw!i?.skipUnauthorizedHandler&&this.config.onUnauthorized&&(this.logger.debug("Received 401, triggering re-authentication"),await this.config.onUnauthorized()),await this.toFanfareError(e)}).catcherFallback(async e=>{throw await this.toFanfareError(e)})}async toFanfareError(e){if(e&&"object"==typeof e&&"response"in e&&e.response instanceof Response){let t=null;const r=await e.response.text().catch(()=>null),i=e;if(r)try{t=JSON.parse(r)}catch{t={message:r}}else if(null!==i.json&&void 0!==i.json&&"object"==typeof i.json)t=i.json;else if("string"==typeof i.text&&i.text.length>0)try{t=JSON.parse(i.text)}catch{t={message:i.text}}const s=("correlationId"in e&&"string"==typeof e.correlationId?e.correlationId:void 0)||void 0;return o.fromResponse(e.response,t,s)}return e&&"object"==typeof e&&"name"in e&&"AbortError"===e.name?new o("Request timed out",n.TIMEOUT,void 0,void 0,"correlationId"in e&&"string"==typeof e.correlationId?e.correlationId:void 0):new o(e&&"object"==typeof e&&"message"in e&&"string"==typeof e.message?e.message:"Network error occurred",n.NETWORK_ERROR,void 0,{originalError:e},e&&"object"==typeof e&&"correlationId"in e&&"string"==typeof e.correlationId?e.correlationId:void 0)}async request(e,t,s,a){const c=l(),g=l(),p=this.config.timeout||3e4,d=await i(),h=function(e){return/^\/(queues|draws|timed-releases|waitlists)\/[^/]+\/enter$/.test(e)}(t),u=new URL(t,this.config.baseUrl).toString(),f=new AbortController,m=setTimeout(()=>f.abort(),p);try{const i={"X-Correlation-Id":c,"X-Request-Id":g,...a?.headers};if(h&&!d&&!i["X-Admission-Key-Thumbprint"])throw new o("Admission proof requires persistent browser crypto support",n.ADMISSION_PROOF_UNAVAILABLE,void 0,{path:t});if(h){const s=await r({url:u,method:e.toUpperCase()});if(!s)throw new o("Admission proof requires persistent browser crypto support",n.ADMISSION_PROOF_UNAVAILABLE,void 0,{path:t});Object.assign(i,s)}if(!h&&function(e){return/^\/auctions\/[^/]+\/bid$/.test(e)}(t)&&d){const t=await r({url:u,method:e.toUpperCase()});t&&Object.assign(i,t)}d&&!i["X-Admission-Key-Thumbprint"]&&(i["X-Admission-Key-Thumbprint"]=d);const l=this.getClient(a).url(t).headers(i).options({signal:a?.signal||f.signal});let p;switch(e){case"get":p=await l.get().json();break;case"post":p=await l.post(s).json();break;case"put":p=await l.put(s).json();break;case"delete":p=await l.delete().json();break;case"patch":p=await l.patch(s).json();break;default:throw new Error(`Unsupported HTTP method: ${e}`)}return p}finally{clearTimeout(m)}}async get(e,t){return this.request("get",e,void 0,t)}async post(e,t,r){return this.request("post",e,t,r)}async put(e,t,r){return this.request("put",e,t,r)}async delete(e,t){return this.request("delete",e,void 0,t)}async patch(e,t,r){return this.request("patch",e,t,r)}updateConfig(e){this.config={...this.config,...e}}getConfig(){return{...this.config}}}function p(e,t){return new g(e,t)}export{g as DefaultHttpClient,p as createHttpClient};
1
+ import t from"wretch";import{retry as e}from"wretch/middlewares";import{buildAdmissionProofHeaders as i,getAdmissionKeyThumbprint as r}from"../security/admission-proof.js";import{generateFingerprint as n}from"../utils/fingerprint.module.js";import{parseRetryAfterSeconds as o,FanfareError as s,ErrorCodes as a}from"./errors.js";import{getLogger as c}from"./logger.js";import{isStorageAvailable as l,generateId as g}from"./utils.js";class h{constructor(t,e){this.logger=c(),this.fingerprint=null,this.inFlightGetRequests=/* @__PURE__ */new Map,this.config=t,this.fingerprintingEnabled=!1!==e?.features?.fingerprinting;const i="fanfare_client_session_id";let r=null;if(l("localStorage"))try{r=window.localStorage.getItem(i)}catch{r=null}if(this.clientSessionId=r||g(),!r&&l("localStorage"))try{window.localStorage.setItem(i,this.clientSessionId)}catch{}this.fingerprintingEnabled?this.initializeFingerprint():this.logger.debug("Browser fingerprinting disabled by configuration")}async initializeFingerprint(){try{const t=await n();this.fingerprint=t.hash,this.logger.debug("Browser fingerprint generated successfully",{fingerprintHash:this.fingerprint})}catch(t){this.logger.debug("Failed to generate browser fingerprint",{error:t instanceof Error?t.message:"Unknown error"}),this.fingerprint=null}}getClient(r,n){const{retryConfig:s={}}=this.config;let a;const c={"Content-Type":"application/json","X-Client-Session-Id":this.clientSessionId,...this.config.headers};return this.fingerprint&&(c["X-Fingerprint"]=this.fingerprint),t(this.config.baseUrl).polyfills({fetch:(t,e)=>globalThis.fetch(t,e)}).options({credentials:this.config.credentials??"include",mode:"cors"}).headers(c).middlewares([(l=this.inFlightGetRequests,g=r?.signal||r?.skipUnauthorizedHandler?void 0:JSON.stringify([this.config.headers,r?.headers,this.fingerprint,n]),t=>(e,i)=>{if(!g||"GET"!==i.method)return t(e,i);const r=`${e}\n${g}`,n=l.get(r);if(n)return n.then(t=>t.clone());const o=t(e,i);return l.set(r,o),o.finally(()=>l.delete(r))}),e({delayTimer:s.delayTimer||1e3,maxAttempts:s.maxRetries||3,delayRamp:(t,e)=>a??Math.random()*Math.min(t*2**(e-1),s.maxDelay||1e4),until:t=>{if(a=void 0,!t)return!1;const e=t.status,i=o(t.headers.get("Retry-After"));return 429===e&&void 0!==i&&i>30||(void 0!==i&&(a=1e3*Math.min(i,30)),!!t.ok||e>=400&&e<500&&429!==e)},onRetry:async t=>{const e=t.options.headers??{};if(!e.DPoP&&!e.dpop)return;const r="string"==typeof t.options.method?t.options.method:"POST",n=this.config.baseUrl,o=t.url.startsWith(n)?new URL(t.url.slice(n.length),n).toString():t.url,s=await i({url:o,method:r});return s?{options:{...t.options,headers:{...e,...s}}}:void 0}})]).catcher(401,async t=>{throw!r?.skipUnauthorizedHandler&&this.config.onUnauthorized&&(this.logger.debug("Received 401, triggering re-authentication"),await this.config.onUnauthorized()),await this.toFanfareError(t)}).catcherFallback(async t=>{throw await this.toFanfareError(t)});var l,g}async toFanfareError(t){if(t&&"object"==typeof t&&"response"in t&&t.response instanceof Response){let e=null;const i=await t.response.text().catch(()=>null),r=t;if(i)try{e=JSON.parse(i)}catch{e={message:i}}else if(null!==r.json&&void 0!==r.json&&"object"==typeof r.json)e=r.json;else if("string"==typeof r.text&&r.text.length>0)try{e=JSON.parse(r.text)}catch{e={message:r.text}}const n=("correlationId"in t&&"string"==typeof t.correlationId?t.correlationId:void 0)||void 0;return s.fromResponse(t.response,e,n)}return t&&"object"==typeof t&&"name"in t&&"AbortError"===t.name?new s("Request timed out",a.TIMEOUT,void 0,void 0,"correlationId"in t&&"string"==typeof t.correlationId?t.correlationId:void 0):new s(t&&"object"==typeof t&&"message"in t&&"string"==typeof t.message?t.message:"Network error occurred",a.NETWORK_ERROR,void 0,{originalError:t},t&&"object"==typeof t&&"correlationId"in t&&"string"==typeof t.correlationId?t.correlationId:void 0)}async request(t,e,n,o){const c=g(),l=g(),h=this.config.timeout||3e4,d=await r(),p=function(t){return/^\/(queues|draws|timed-releases)\/[^/]+\/enter$/.test(t)}(e),f=new URL(e,this.config.baseUrl).toString(),u=new AbortController,m=setTimeout(()=>u.abort(),h);try{const r={"X-Correlation-Id":c,"X-Request-Id":l,...o?.headers};if(p&&!d&&!r["X-Admission-Key-Thumbprint"])throw new s("Admission proof requires persistent browser crypto support",a.ADMISSION_PROOF_UNAVAILABLE,void 0,{path:e});if(p){const n=await i({url:f,method:t.toUpperCase()});if(!n)throw new s("Admission proof requires persistent browser crypto support",a.ADMISSION_PROOF_UNAVAILABLE,void 0,{path:e});Object.assign(r,n)}if(!p&&function(t){return/^\/auctions\/[^/]+\/bid$/.test(t)}(e)&&d){const e=await i({url:f,method:t.toUpperCase()});e&&Object.assign(r,e)}d&&!r["X-Admission-Key-Thumbprint"]&&(r["X-Admission-Key-Thumbprint"]=d);const g=this.getClient(o,d).url(e).headers(r).options({signal:o?.signal||u.signal});return await("get"===t||"delete"===t?g[t]():g[t](n)).json()}finally{clearTimeout(m)}}async get(t,e){return this.request("get",t,void 0,e)}async post(t,e,i){return this.request("post",t,e,i)}async put(t,e,i){return this.request("put",t,e,i)}async delete(t,e){return this.request("delete",t,void 0,e)}async patch(t,e,i){return this.request("patch",t,e,i)}updateConfig(t){this.config={...this.config,...t}}getConfig(){return{...this.config}}}function d(t,e){return new h(t,e)}export{h as DefaultHttpClient,d as createHttpClient};
@@ -0,0 +1,21 @@
1
+ export interface PollSchedulerOptions {
2
+ baseMs: number;
3
+ jitterRatio?: number;
4
+ backoffMultiplier?: number;
5
+ maxBackoffFactor?: number;
6
+ retryAfterCapMs?: number;
7
+ /** When true, failures do NOT stretch the interval (grant-expecting override). */
8
+ suppressBackoff?: () => boolean;
9
+ /** Called on entering (>=2 consecutive failures) and leaving (first success) degraded. */
10
+ onDegradedChange?: (degraded: boolean) => void;
11
+ run: () => Promise<void>;
12
+ /** Return false to stop rescheduling (generation guard hook). */
13
+ shouldContinue: () => boolean;
14
+ }
15
+ export interface PollScheduler {
16
+ start(): void;
17
+ stop(): void;
18
+ /** immediate out-of-band run, e.g. on tab focus */
19
+ poke(): void;
20
+ }
21
+ export declare function createPollScheduler(opts: PollSchedulerOptions): PollScheduler;
@@ -0,0 +1 @@
1
+ import{FanfareError as t}from"./errors.js";import{jitteredIntervalMs as e}from"./utils.js";function r(r){const o=r.jitterRatio??.2,i=r.backoffMultiplier??2,n=r.maxBackoffFactor??6,a=r.retryAfterCapMs??6e4;let f,s,u,c=0,d=0,m=!1,p=!1;function l(){void 0!==f&&(clearTimeout(f),f=void 0)}function M(t){t===c&&r.shouldContinue()&&(l(),f=setTimeout(()=>{f=void 0,h(t)},function(){const t=!0===r.suppressBackoff?.(),a=t||0===d?1:Math.min(i**d,n),f=void 0===u?r.baseMs*a:t?Math.min(u,r.baseMs):u,s=e(f,o);return t?Math.min(s,r.baseMs):s}()))}async function h(e){if(s===e)return;let o;s=e;try{await r.run()}catch(i){o=i}finally{s===e&&(s=void 0)}e===c&&(void 0===o?(d=0,u=void 0,m&&(m=!1,r.onDegradedChange?.(!1))):(d+=1,u=function(e,r){if(e instanceof t&&"number"==typeof e.retryAfter&&Number.isFinite(e.retryAfter))return Math.min(1e3*e.retryAfter,r)}(o,a),d>=2&&!m&&(m=!0,r.onDegradedChange?.(!0))),M(e))}return{start(){p=!0,c+=1,l(),d=0,m=!1,u=void 0,h(c)},stop(){p=!1,c+=1,l()},poke(){p&&(l(),s!==c&&h(c))}}}export{r as createPollScheduler};
@@ -17,6 +17,8 @@ export declare function createDeferred<T>(): {
17
17
  * Sleep for specified milliseconds
18
18
  */
19
19
  export declare function sleep(ms: number): Promise<void>;
20
+ /** Returns baseMs randomized within +/-ratio for de-synchronizing fixed-interval polling across clients. */
21
+ export declare const jitteredIntervalMs: (baseMs: number, ratio?: number, random?: () => number) => number;
20
22
  /**
21
23
  * Retry a function with exponential backoff
22
24
  */