@fanfare-io/fanfare-sdk-core 0.13.0 → 0.14.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 (54) hide show
  1. package/README.md +51 -0
  2. package/dist/adapters/beacon-lifecycle.js +1 -0
  3. package/dist/appointments/appointment.driver.js +1 -1
  4. package/dist/appointments/appointment.module.js +1 -1
  5. package/dist/auctions/auction.driver.js +1 -1
  6. package/dist/auctions/auction.module.js +1 -1
  7. package/dist/auctions/auction.sequence.d.ts +50 -3
  8. package/dist/auth/auth.module.js +1 -1
  9. package/dist/beacon/beacon.module.js +1 -1
  10. package/dist/beacon/types.d.ts +8 -1
  11. package/dist/core/client.js +2 -2
  12. package/dist/core/error-disposition.js +1 -1
  13. package/dist/core/errors.d.ts +2 -4
  14. package/dist/core/http.js +1 -1
  15. package/dist/core/logger.js +1 -1
  16. package/dist/draws/draw.driver.js +1 -1
  17. package/dist/draws/draw.module.js +1 -1
  18. package/dist/draws/draw.sequence.d.ts +63 -99
  19. package/dist/draws/public.d.ts +1 -1
  20. package/dist/draws/types.d.ts +2 -14
  21. package/dist/experiences/distribution-monitor.types.d.ts +4 -13
  22. package/dist/experiences/dom-bridge.js +1 -1
  23. package/dist/experiences/experience.module.js +1 -1
  24. package/dist/experiences/journey-view.js +1 -1
  25. package/dist/experiences/journey.js +1 -1
  26. package/dist/experiences/journey.machine.d.ts +2 -7
  27. package/dist/experiences/journey.machine.js +1 -1
  28. package/dist/experiences/journey.types.d.ts +3 -3
  29. package/dist/experiences/public.d.ts +3 -2
  30. package/dist/experiences/sequence-driver.js +1 -0
  31. package/dist/internals.d.ts +1 -0
  32. package/dist/internals.js +1 -1
  33. package/dist/payment/checkout-contract.js +1 -1
  34. package/dist/payment/settlement.d.ts +14 -0
  35. package/dist/payment/settlement.js +1 -0
  36. package/dist/payment/types.d.ts +13 -35
  37. package/dist/queues/queue.driver.js +1 -1
  38. package/dist/queues/queue.module.js +1 -1
  39. package/dist/queues/queue.sequence.d.ts +62 -100
  40. package/dist/queues/types.d.ts +2 -15
  41. package/dist/ssr/ssr-sdk.js +1 -1
  42. package/dist/state/capability-token-registry.js +1 -1
  43. package/dist/state/events.d.ts +19 -1
  44. package/dist/state/events.js +1 -1
  45. package/dist/timed-releases/timed-release.driver.js +1 -1
  46. package/dist/timed-releases/timed-release.module.js +1 -1
  47. package/dist/timed-releases/timed-release.sequence.d.ts +50 -3
  48. package/dist/types/index.d.ts +3 -4
  49. package/dist/version.d.ts +1 -1
  50. package/dist/version.js +1 -1
  51. package/dist/waitlists/waitlist.module.js +1 -1
  52. package/package.json +4 -4
  53. package/dist/payment/reservations.module.d.ts +0 -17
  54. package/dist/payment/reservations.module.js +0 -1
package/README.md CHANGED
@@ -42,6 +42,57 @@ The `./errors` subpath exposes the client-side error policy the UI adapters rout
42
42
 
43
43
  `classify` takes no React/i18n dependency; the translator is injected as a structural `Translate` type.
44
44
 
45
+ ## Beacon analytics
46
+
47
+ `sdk.beacon.track(event)` queues a single analytics event. Queued events are batched client-side
48
+ and flushed on batch size, on a timer, and on page visibility change or unload.
49
+ `sdk.beacon.trackBatch(events)` bypasses the batching queue and sends its events immediately; only
50
+ `track` is batched.
51
+
52
+ The SDK also forwards eight lifecycle events on its own, derived from the experience and journey
53
+ event bus. What each one counts:
54
+
55
+ | Event | Counts |
56
+ | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
57
+ | `pageView` | Each experience widget mount, plus any `pageView` an integration tracks itself. Not distinct visitors and not page loads. |
58
+ | `experienceEntered` | Entering an experience: once per session per experience. A journey resumed after a reload does not re-emit, so this under-reports across reloads. |
59
+ | `experienceLeft` | An explicit leave only. Closing the tab emits nothing. |
60
+ | `sequenceGranted` | A grant from a queue, draw, auction or timed release, tagged with the distribution type that produced it. |
61
+ | `sequenceEnded` | A sequence ending, tagged with the distribution type, the outcome, and a denial reason where there is one. In a multi-tab browser an expiry emits once per open tab, so this is an upper bound. |
62
+ | `distributionEntered` | Each successful entry into a distribution, tagged with the distribution type. Counted per call: an appointment cancelled and rebooked counts twice. |
63
+ | `waitlistEntered` | Joining a waitlist. A replay of the same entry — the same waitlist id and entry timestamp — is suppressed within one adapter instance. Leaving and rejoining is a new entry and counts again, and destroying and re-initialising the SDK clears the suppression. |
64
+ | `waitlistLeft` | Leaving a waitlist. |
65
+
66
+ `pageView` carries a `pageType` of `"hosted"` (a Fanfare-hosted page) or `"embed"` (a widget
67
+ embedded in the merchant's own site). The `ExperienceWidget` components default to `"embed"` and
68
+ accept a `pageType` prop. The split becomes meaningful once a hosted surface passes
69
+ `pageType="hosted"`; no Fanfare-hosted surface sets it yet, so today every widget mount reports
70
+ `"embed"` unless an integration passes the prop itself.
71
+
72
+ Every beacon event carries page context and marketing parameters read from the browser: the full
73
+ page URL including its query string, the path, the page title, the referrer, and the `utm_source`,
74
+ `utm_medium`, `utm_campaign`, `utm_term` and `utm_content` parameters on the URL. Set
75
+ `beacon.disablePageContext: true` to stop the URL, path, title and referrer, and
76
+ `beacon.disableMarketingParams: true` to stop the `utm_*` extraction.
77
+
78
+ Set `beacon.disableLifecycleEvents: true` in the SDK config to stop all eight. It does not affect
79
+ `sdk.beacon.track` calls an integration makes itself, nor subscribers on the SDK event bus.
80
+ The option is not reachable from the `<fanfare-experience-widget>` web component: the
81
+ `registerWebComponents` options expose no `beacon` configuration, so those integrations cannot
82
+ disable lifecycle events today.
83
+
84
+ ```ts
85
+ const sdk = await initFanfare({
86
+ organizationId: "org_123",
87
+ publishableKey: "pk_live_123",
88
+ beacon: { disableLifecycleEvents: true },
89
+ });
90
+ ```
91
+
92
+ The framework adapters expose a tracker already bound to the surrounding experience:
93
+ `useBeaconTracker` from `@fanfare-io/fanfare-sdk-react` and `createBeaconTracker` from
94
+ `@fanfare-io/fanfare-sdk-solid`.
95
+
45
96
  ## Documentation
46
97
 
47
98
  - [Core SDK quickstart](https://docs.fanfare.io/sdk/core/quickstart)
@@ -0,0 +1 @@
1
+ import{BEACON_SEQUENCE_END_REASONS as e}from"@fanfare-io/fanfare-sdk-contracts/beacon";import{GrantProducingMechanisms as t}from"@fanfare-io/fanfare-sdk-contracts/consumer-me";import{getLogger as n}from"../core/logger.js";const i=new Set(t),r=new Set(e);function d(e){const t=[],d=/* @__PURE__ */new Map;function o(t){try{e(t).catch(e=>{n().debug("Beacon lifecycle event was not accepted",{eventName:t.eventName,error:e})})}catch(i){n().debug("Beacon lifecycle event could not be tracked",{eventName:t.eventName,error:i})}}return{id:"beacon-lifecycle",init(e){if(t.length>0)throw new Error("Beacon lifecycle adapter already initialized. Create a new adapter instance per SDK via createBeaconLifecycleAdapter().");t.push(e.on("experience:entered",e=>{o({eventName:"experienceEntered",experienceId:e.experienceId})})),t.push(e.on("experience:left",e=>{o({eventName:"experienceLeft",experienceId:e.experienceId})})),t.push(e.on("journey:granted",e=>{const t=(r=e.distributionType,i.has(r)?r:void 0);var r;void 0!==t?o({eventName:"sequenceGranted",experienceId:e.experienceId,sequenceId:e.sequenceId,eventProperties:{distributionType:t}}):n().debug("Dropping sequenceGranted with a non-grant-producing distribution type",{distributionType:e.distributionType})})),t.push(e.on("journey:ended",e=>{const t=void 0!==(n=e.outcome.reason)&&r.has(n)?n:void 0;var n;o({eventName:"sequenceEnded",experienceId:e.experienceId,sequenceId:e.sequenceId,eventProperties:{outcome:e.outcome.type,...void 0!==e.outcome.distributionType&&{distributionType:e.outcome.distributionType},...void 0!==t&&{denialReason:t}}})})),t.push(e.on("experience:distribution-entered",e=>{void 0!==e.experienceId?o({eventName:"distributionEntered",experienceId:e.experienceId,...void 0!==e.sequenceId&&{sequenceId:e.sequenceId},eventProperties:{distributionType:e.distributionType}}):n().debug("Dropping distributionEntered with no experience attribution",{distributionId:e.distributionId})})),t.push(e.on("experience:widget-mounted",e=>{const{experienceId:t,pageType:n}=e;o({eventName:"pageView",experienceId:t,eventProperties:{pageType:n}})})),t.push(e.on("waitlist:entered",e=>{void 0!==e.experienceId?d.get(e.waitlistId)!==e.enteredAt&&(d.set(e.waitlistId,e.enteredAt),o({eventName:"waitlistEntered",experienceId:e.experienceId,sequenceId:e.sequenceId})):n().debug("Dropping waitlistEntered with no experience attribution",{waitlistId:e.waitlistId})})),t.push(e.on("waitlist:left",e=>{void 0!==e.experienceId?o({eventName:"waitlistLeft",experienceId:e.experienceId,...void 0!==e.sequenceId&&{sequenceId:e.sequenceId}}):n().debug("Dropping waitlistLeft with no experience attribution",{waitlistId:e.waitlistId})}))},destroy(){for(const e of t)e();t.length=0,d.clear()}}}export{d as createBeaconLifecycleAdapter};
@@ -1 +1 @@
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};
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.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};
@@ -1 +1 @@
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","cancelled","no_show"]),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.nullish(t.string()),locationId:t.nullish(t.string()),locationName:t.nullish(t.string()),joinUrl:t.nullish(t.string()),startTime:t.nullish(t.string()),endTime:t.nullish(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.observedBooking=/* @__PURE__ */new Map,this.destroyed=!1,this.http=t,this.pollingIntervalMs=o?.pollingIntervalMs??3e4}async get(t,n){try{const o=await this.http.get(`/appointments/${t}`,n),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(e){if(this.logger.error("Failed to get appointment",{appointmentId:t,error:e}),o(e))throw e;throw i.networkError("Failed to get appointment",{error:e})}}async getSlots(t,n,e){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()}`,e);return a(d,i,`/appointments/${t}/slots`).slots.map(u)}catch(r){if(this.logger.error("Failed to get appointment slots",{appointmentId:t,error:r}),o(r))throw r;throw i.networkError("Failed to get appointment slots",{error:r})}}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,r){try{const o=await this.http.post(`/appointments/${t}/book`,{slotId:n,locationId:e??null},r);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(s){if(this.logger.error("Failed to book appointment",{appointmentId:t,slotId:n,locationId:e,error:s}),o(s))throw s;throw i.networkError("Failed to book appointment",{error:s})}}async cancel(t,n,e,r,s){try{await this.http.post(`/appointments/${t}/cancel`,{slotId:n,locationId:e??null,reason:r??null},s)}catch(a){if(this.logger.error("Failed to cancel appointment",{appointmentId:t,slotId:n,locationId:e,error:a}),o(a))throw a;throw i.networkError("Failed to cancel appointment",{error:a})}}async reschedule(t,n,e,r){try{const o=await this.http.post(`/appointments/${t}/reschedule`,{newSlotId:n,newLocationId:e??null},r),i=a(g,o,`/appointments/${t}/reschedule`),s=await this.getMe(t);return s?{...s,confirmationCode:i.confirmationCode}:{bookingId:i.bookingId,appointmentId:t,slotId:i.newSlotId,locationId:e,startTime:"",endTime:"",confirmationCode:i.confirmationCode,consumerStatus:i.status}}catch(s){if(this.logger.error("Failed to reschedule appointment",{appointmentId:t,newSlotId:n,newLocationId:e,error:s}),o(s))throw s;throw i.networkError("Failed to reschedule appointment",{error:s})}}startMonitoring(t,o,i,n){if(this.destroyed)return;this.monitorRuntime.start(t,o,i),this.stopPolling(t);const e=this.monitorRuntime.getDisplayAtom(t)?.get();this.observedBooking.set(t,"appointment"===e?.type&&void 0!==e.booking);const s=(this.monitorGenerations.get(t)??0)+1;this.monitorGenerations.set(t,s);const a=r({baseMs:this.pollingIntervalMs,suppressBackoff:()=>!1,onDegradedChange:n,shouldContinue:()=>!this.destroyed&&this.monitorGenerations.get(t)===s&&this.pollSchedulers.has(t),run:async()=>{if(!this.destroyed&&this.monitorGenerations.get(t)===s)try{const o=await this.getMe(t);if(this.destroyed||this.monitorGenerations.get(t)!==s)return;const i=this.monitorRuntime.getDisplayAtom(t);if(!i)return;if(!o)return void(this.observedBooking.get(t)&&(this.monitorRuntime.notify(t,{type:"ended",outcome:{type:"closed"}}),this.stopMonitoring(t)));if(this.observedBooking.set(t,!0),"cancelled"===o.consumerStatus||"no_show"===o.consumerStatus)return this.monitorRuntime.notify(t,{type:"ended",outcome:{type:o.consumerStatus}}),void this.stopMonitoring(t);const n=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:n})}catch(o){throw this.logger.error("Appointment polling tick failed",{id:t,error:o}),o}}});this.pollSchedulers.set(t,a),a.start()}stopMonitoring(t){this.stopPolling(t),this.monitorRuntime.stop(t),this.observedBooking.delete(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(),this.observedBooking.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
+ import*as t from"valibot";import{isFanfareError as o,createError as i}from"../core/errors.js";import{getLogger as e}from"../core/logger.js";import{parseResponse as n}from"../core/parse-response.js";import{createPollScheduler as r}from"../core/poll-scheduler.js";import{DistributionMonitorRuntime as s}from"../experiences/distribution-monitor.runtime.js";import{getEventBus as a}from"../state/events.js";function l(t,o,i){const e=n(t,o,{endpoint:i},{throwOnFailure:!0});if(!e.ok)throw e.error;return e.value}const d=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()}),m=t.object({slots:t.array(d)}),c=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())}),p=t.picklist(["booked","checked_in","completed","cancelled","no_show"]),h=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:p,createdAt:t.string()}),u=t.object({booking:t.nullish(t.object({bookingId:t.string(),appointmentId:t.string(),consumerId:t.string(),slotId:t.nullish(t.string()),locationId:t.nullish(t.string()),locationName:t.nullish(t.string()),joinUrl:t.nullish(t.string()),startTime:t.nullish(t.string()),endTime:t.nullish(t.string()),confirmationCode:t.nullish(t.string()),status:p}))}),g=t.object({bookingId:t.string(),previousSlotId:t.string(),newSlotId:t.string(),confirmationCode:t.string(),status:p});function I(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 b{constructor(t,o){this.logger=e(),this.monitorRuntime=new s,this.pollSchedulers=/* @__PURE__ */new Map,this.monitorGenerations=/* @__PURE__ */new Map,this.observedBooking=/* @__PURE__ */new Map,this.destroyed=!1,this.http=t,this.pollingIntervalMs=o?.pollingIntervalMs??3e4}async get(t,e){try{const o=await this.http.get(`/appointments/${t}`,e),i=l(c,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,e,n){try{const o=new URLSearchParams({startDate:e.startDate,endDate:e.endDate});e.timezone&&o.set("timezone",e.timezone);const i=await this.http.get(`/appointments/${t}/slots?${o.toString()}`,n);return l(m,i,`/appointments/${t}/slots`).slots.map(I)}catch(r){if(this.logger.error("Failed to get appointment slots",{appointmentId:t,error:r}),o(r))throw r;throw i.networkError("Failed to get appointment slots",{error:r})}}async getMe(t){try{const o=await this.http.get(`/appointments/${t}/me`),i=l(u,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(e){if(this.logger.error("Failed to get current booking",{appointmentId:t,error:e}),o(e))throw e;throw i.networkError("Failed to get current booking",{error:e})}}async book(t,e,n,r,s){try{const o=await this.http.post(`/appointments/${t}/book`,{slotId:e,locationId:n??null},r),i=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}}(l(h,o,`/appointments/${t}/book`));return a().emit("experience:distribution-entered",{distributionType:"appointment",distributionId:t,...s}),i}catch(d){if(this.logger.error("Failed to book appointment",{appointmentId:t,slotId:e,locationId:n,error:d}),o(d))throw d;throw i.networkError("Failed to book appointment",{error:d})}}async cancel(t,e,n,r,s){try{await this.http.post(`/appointments/${t}/cancel`,{slotId:e,locationId:n??null,reason:r??null},s)}catch(a){if(this.logger.error("Failed to cancel appointment",{appointmentId:t,slotId:e,locationId:n,error:a}),o(a))throw a;throw i.networkError("Failed to cancel appointment",{error:a})}}async reschedule(t,e,n,r){try{const o=await this.http.post(`/appointments/${t}/reschedule`,{newSlotId:e,newLocationId:n??null},r),s=l(g,o,`/appointments/${t}/reschedule`),a=await this.getMe(t);if(!a)throw i.networkError("Reschedule succeeded but booking could not be reloaded");if(this.isMonitoring(t)){const o=this.monitorRuntime.getDisplayAtom(t);this.startMonitoring(t,o?{displayAtom:o}:void 0)}return{...a,confirmationCode:s.confirmationCode}}catch(s){if(this.logger.error("Failed to reschedule appointment",{appointmentId:t,newSlotId:e,newLocationId:n,error:s}),o(s))throw s;throw i.networkError("Failed to reschedule appointment",{error:s})}}startMonitoring(t,o,i,e){if(this.destroyed)return;this.monitorRuntime.start(t,o,i),this.stopPolling(t);const n=this.monitorRuntime.getDisplayAtom(t)?.get();this.observedBooking.set(t,"appointment"===n?.type&&void 0!==n.booking);const s=(this.monitorGenerations.get(t)??0)+1;this.monitorGenerations.set(t,s);const a=r({baseMs:this.pollingIntervalMs,suppressBackoff:()=>!1,onDegradedChange:e,shouldContinue:()=>!this.destroyed&&this.monitorGenerations.get(t)===s&&this.pollSchedulers.has(t),run:async()=>{if(!this.destroyed&&this.monitorGenerations.get(t)===s)try{const o=await this.getMe(t);if(this.destroyed||this.monitorGenerations.get(t)!==s)return;const i=this.monitorRuntime.getDisplayAtom(t);if(!o)return void(this.observedBooking.get(t)&&(this.monitorRuntime.notify(t,{type:"ended",outcome:{type:"closed"}}),this.stopMonitoring(t)));if(this.observedBooking.set(t,!0),"cancelled"===o.consumerStatus||"no_show"===o.consumerStatus||"completed"===o.consumerStatus)return this.monitorRuntime.notify(t,{type:"ended",outcome:{type:o.consumerStatus}}),void this.stopMonitoring(t);const e=Math.max(0,Date.parse(o.startTime)-Date.now());if(!i)return;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:e})}catch(o){throw this.logger.error("Appointment polling tick failed",{id:t,error:o}),o}}});this.pollSchedulers.set(t,a),a.start()}stopMonitoring(t){this.stopPolling(t),this.monitorRuntime.stop(t),this.observedBooking.delete(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(),this.observedBooking.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{b as AppointmentManagementModule};
@@ -1 +1 @@
1
- import{AuctionDistributionResults as t}from"@fanfare-io/fanfare-sdk-contracts";import{createError as i}from"../core/errors.js";import{isMoneyString as e,addMoney as n}from"../core/money.js";import{isGrantPendingWin as s}from"./auction-status.js";import{isUnresolvedAuctionConsumer as o}from"./auction.sequence.js";function a(t){return!t||("lost"===t.type||"closed"===t.type)&&!t.reason}function r(t){return new p(t)}function c(e){const{distribution:n,consumer:s,grant:r,outcome:c}=e;if(!n)return{phase:"unavailable"};const u=function(t){if("auction"!==t.type)throw i.validationError("Auction driver received a non-auction distribution");return t}(n),d=s&&"auction"===s.mechanism?function(t){const{mechanism:i,...e}=t;return e}(s):h(n);if(r)return"completed"===c?.type?m(u,d,c):r.expiresAt&&Date.now()>new Date(r.expiresAt).getTime()?"open"===u.lifecycle?{phase:"enterable",mechanism:"auction",distribution:u,consumer:h(u)}:m(u,d,{type:"expired"}):{phase:"granted",mechanism:"auction",distribution:u,consumer:d,grant:r};if("closed"===u.lifecycle){if("not_bid"===d.status&&u.result===t.FLOOR_UNSOLD&&a(c))return m(u,d,{type:"lost",reason:t.FLOOR_UNSOLD});if("auction"===s?.mechanism&&!c)return m(u,d,{type:"lost"})}const p=c??function(t){return"lost"===t.status?{type:"lost"}:void 0}(d);return p?m(u,d,p):"settling"===u.lifecycle?{phase:"settling",mechanism:"auction",distribution:u,consumer:d}:o(d)?{phase:"participating",mechanism:"auction",distribution:u,consumer:d}:"scheduled"===u.lifecycle?{phase:"scheduled",mechanism:"auction",distribution:u}:"open"===u.lifecycle?{phase:"enterable",mechanism:"auction",distribution:u,consumer:d}:m(u,d,{type:"closed"})}function u(t,i){return"auction"===t?.mechanism?{...t,status:"won"}:{mechanism:"auction",status:"won",id:i.id,distributionId:i.id}}function d(t,i,e){return t&&"auction"===t.mechanism&&i?"won"===i.type?{...t,status:"won"}:!e?.finalize&&"not_bid"===t.status&&a(i)?t:{...t,status:"lost"}:t}class p{constructor(t){this.deps=t,this.mechanism="auction"}project(t){return c(t)}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:t=>this.bid(t)};case"participating":return{phase:"participating",mechanism:"auction",consumer:n.consumer,display$:e.displayAtom,bid:t=>this.bid(t)};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(t){this.activeMonitorId=t.participation.id,this.deps.auctions.startMonitoring(t.participation.id,{displayAtom:t.displayAtom},t.onUpdate,t.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 t=this.currentAuctionSequence("enterable","No open auction to enter").distribution;await this.deps.auctions.enter(t.id,void 0),await this.deps.auctions.status(t.id);const i={mechanism:"auction",status:"not_bid",id:e=t.id,distributionId:e};var e;this.deps.ensureDisplayAtom({id:t.id,type:"auction"}),this.deps.commit(c({distribution:t,consumer:i})),this.deps.emitEvent("sequence_change","success","user","Entered auction",{distributionId:t.id})}async leave(){await this.deps.runSerializedOperation(async()=>{await this.leaveWithinOperation()})}async leaveWithinOperation(){const t=this.currentAuctionSequence("participating"),i=t.consumer.distributionId??t.consumer.id??t.distribution.id;await this.deps.auctions.leave(i),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(t){await this.deps.runSerializedOperation(async()=>{const i=this.currentAuctionSequenceForBid(),o=i.consumer.distributionId??i.consumer.id??i.distribution.id,a=await this.deps.auctions.bid(o,t);if("won"===a.status||"lost"===a.status){const t=this.deps.getCurrentSequence();return void(t&&("enterable"===t.phase||"participating"===t.phase||"settling"===t.phase)&&"mechanism"in t&&"auction"===t.mechanism&&t.distribution.id===o&&("won"===a.status&&a.admissionGrant?this.deps.applyMonitorUpdate({type:"granted",token:a.admissionGrant}):s(a)||this.deps.applyMonitorUpdate({type:"ended",outcome:{type:a.status}})))}const r=function(t){switch(t){case"not_bid":case"bidding":case"winning":case"outbid":return t;case"won":case"lost":return}}(a.status);if(void 0===r)return;const u="auction"===i.distribution.details?.type?i.distribution.details:void 0,d=u?.bidIncrement,p=a.highestBid&&d&&e(a.highestBid)&&e(d)?n(a.highestBid,d):void 0,h=this.deps.getDisplayState(),m="auction"===h?.type?h.settleAt:void 0,l=this.deps.ensureDisplayAtom({id:o,type:"auction"});"enterable"===i.phase&&this.deps.commit(c({distribution:i.distribution,consumer:{mechanism:"auction",status:r,id:o,distributionId:o,currentBid:t,highestBid:a.highestBid}})),l.set({type:"auction",currentBid:t,highestBid:a.highestBid,bidCount:a.bidCount,status:r,currencyCode:u?.currencyCode,bidIncrement:d,minNextBid:p,closeAt:u?.closeAt??u?.endsAt,settleAt:m})})}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 h(t){return{status:"not_bid",distributionId:t.id}}function m(t,i,e){return{phase:"ended",mechanism:"auction",distribution:t,consumer:i,outcome:{...e,distributionType:e.distributionType??t.type,distributionId:e.distributionId??t.id,at:e.at??/* @__PURE__ */(new Date).toISOString()}}}export{r as createAuctionDriver,a as isUpgradeableGenericLoss,c as projectAuctionSequence,u as withAuctionGrantedConsumer,d as withAuctionTerminalConsumer};
1
+ import{AuctionDistributionResults as t}from"@fanfare-io/fanfare-sdk-contracts";import{createError as i}from"../core/errors.js";import{isMoneyString as e,addMoney as n}from"../core/money.js";import{buildGrantPaymentAffordance as s}from"../experiences/sequence-driver.js";import{isGrantPendingWin as o}from"./auction-status.js";import{isUnresolvedAuctionConsumer as a}from"./auction.sequence.js";function r(t){return!t||("lost"===t.type||"closed"===t.type)&&!t.reason}function c(t){return new p(t)}function u(e){const{distribution:n,consumer:s,grant:o,settlement:c,selection:u,outcome:d}=e;if(!n)return{phase:"unavailable"};const p=function(t){if("auction"!==t.type)throw i.validationError("Auction driver received a non-auction distribution");return t}(n),l=s&&"auction"===s.mechanism?function(t){const{mechanism:i,...e}=t;return e}(s):m(n);if(o)return"completed"===d?.type?h(p,l,d):o.expiresAt&&Date.now()>new Date(o.expiresAt).getTime()?"open"===p.lifecycle?{phase:"enterable",mechanism:"auction",distribution:p,consumer:m(p)}:h(p,l,{type:"expired"}):c&&u?{phase:"granted",mechanism:"auction",distribution:p,consumer:l,grant:o,settlement:c,selection:u}:{phase:"unavailable"};if("closed"===p.lifecycle){if("not_bid"===l.status&&p.result===t.FLOOR_UNSOLD&&r(d))return h(p,l,{type:"lost",reason:t.FLOOR_UNSOLD});if("auction"===s?.mechanism&&!d)return h(p,l,{type:"lost"})}const b=d??function(t){return"lost"===t.status?{type:"lost"}:void 0}(l);return b?h(p,l,b):"settling"===p.lifecycle?{phase:"settling",mechanism:"auction",distribution:p,consumer:l}:a(l)?{phase:"participating",mechanism:"auction",distribution:p,consumer:l}:"scheduled"===p.lifecycle?{phase:"scheduled",mechanism:"auction",distribution:p}:"open"===p.lifecycle?{phase:"enterable",mechanism:"auction",distribution:p,consumer:l}:h(p,l,{type:"closed"})}function d(t,i,e){return t&&"auction"===t.mechanism&&i?"won"===i.type?{...t,status:"won"}:!e?.finalize&&"not_bid"===t.status&&r(i)?t:{...t,status:"lost"}:t}class p{constructor(t){this.deps=t,this.mechanism="auction"}project(t){return u(t)}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:t=>this.bid(t)};case"participating":return{phase:"participating",mechanism:"auction",consumer:n.consumer,display$:e.displayAtom,bid:t=>this.bid(t)};case"settling":return{phase:"settling",mechanism:"auction",distribution:n.distribution,consumer:n.consumer};case"granted":{const t={phase:"granted",mechanism:"auction",distribution:n.distribution,consumer:n.consumer,grant:n.grant,selection:n.selection},{settlement:i}=n;if("internal"===i.processor&&"at_end"===i.mode){const o=s(i,n.pendingAction,e);return o?{...t,settlement:i,claim:()=>e.claim(),payment:o}:{...t,settlement:i,claim:()=>e.claim()}}return"external"===i.processor?{...t,settlement:i,claim:()=>e.claim()}:{...t,settlement:i}}case"ended":return{phase:"ended",mechanism:"auction",consumer:n.consumer,outcome:n.outcome}}}startMonitoring(t){this.activeMonitorId=t.participation.id,this.deps.auctions.startMonitoring(t.participation.id,{displayAtom:t.displayAtom},t.onUpdate,t.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 t=this.currentAuctionSequence("enterable","No open auction to enter").distribution;await this.deps.auctions.enter(t.id,void 0),await this.deps.auctions.status(t.id);const i={mechanism:"auction",status:"not_bid",id:e=t.id,distributionId:e};var e;this.deps.ensureDisplayAtom({id:t.id,type:"auction"}),this.deps.commit(u({distribution:t,consumer:i})),this.deps.emitEvent("sequence_change","success","user","Entered auction",{distributionId:t.id})}async leave(){await this.deps.runSerializedOperation(async()=>{await this.leaveWithinOperation()})}async leaveWithinOperation(){const t=this.currentAuctionSequence("participating"),i=t.consumer.distributionId??t.consumer.id??t.distribution.id;await this.deps.auctions.leave(i),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(t){await this.deps.runSerializedOperation(async()=>{const i=this.currentAuctionSequenceForBid(),s=i.consumer.distributionId??i.consumer.id??i.distribution.id,a=await this.deps.auctions.bid(s,t);if("won"===a.status||"lost"===a.status){const t=this.deps.getCurrentSequence();return void(t&&("enterable"===t.phase||"participating"===t.phase||"settling"===t.phase)&&"mechanism"in t&&"auction"===t.mechanism&&t.distribution.id===s&&("won"===a.status&&a.admissionGrant?this.deps.applyMonitorUpdate({type:"granted",token:a.admissionGrant}):o(a)||this.deps.applyMonitorUpdate({type:"ended",outcome:{type:a.status}})))}const r=function(t){switch(t){case"not_bid":case"bidding":case"winning":case"outbid":return t;case"won":case"lost":return}}(a.status);if(void 0===r)return;const c="auction"===i.distribution.details?.type?i.distribution.details:void 0,d=c?.bidIncrement,p=a.highestBid&&d&&e(a.highestBid)&&e(d)?n(a.highestBid,d):void 0,m=this.deps.getDisplayState(),h="auction"===m?.type?m.settleAt:void 0,l=this.deps.ensureDisplayAtom({id:s,type:"auction"});"enterable"===i.phase&&this.deps.commit(u({distribution:i.distribution,consumer:{mechanism:"auction",status:r,id:s,distributionId:s,currentBid:t,highestBid:a.highestBid}})),l.set({type:"auction",currentBid:t,highestBid:a.highestBid,bidCount:a.bidCount,status:r,currencyCode:c?.currencyCode,bidIncrement:d,minNextBid:p,closeAt:c?.closeAt??c?.endsAt,settleAt:h})})}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 m(t){return{status:"not_bid",distributionId:t.id}}function h(t,i,e){return{phase:"ended",mechanism:"auction",distribution:t,consumer:i,outcome:{...e,distributionType:e.distributionType??t.type,distributionId:e.distributionId??t.id,at:e.at??/* @__PURE__ */(new Date).toISOString()}}}export{c as createAuctionDriver,r as isUpgradeableGenericLoss,u as projectAuctionSequence,d as withAuctionTerminalConsumer};
@@ -1 +1 @@
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 d,isMoneyLessThanOrEqualTo as u,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";import{isGrantPendingWin as b,isTerminalAuctionStatus as y,toAuctionConsumerStatus as f}from"./auction-status.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){void 0!==i.trackedParticipation&&(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("outbid"!==i.status)return!1;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)&&"outbid"===i.status}return"outbid"!==t.status&&"outbid"===i.status}buildBidTransition(t,i,e,s,n,a){const o={type:"auction:bid-placed",payload:{auctionId:t,amount:e,status:i.status}};if("won"===i.status&&!b(i)||"lost"===i.status){const a=this.buildTerminalTransition(t,i.status,s,{currentBid:e,highestBid:i.highestBid,bidCount:i.bidCount,admissionGrant:n});return{...a,events:[o,...a.events]}}return{trackedParticipation:{auctionId:t,enteredAt:a?.enteredAt??/* @__PURE__ */(new Date).toISOString(),status:i.status,currentBid:e,highestBid:i.highestBid,bidCount:i.bidCount,lastBidAt:/* @__PURE__ */(new Date).toISOString(),metadata:a?.metadata,fingerprint:a?.fingerprint},displayState:this.buildDisplayState(s,{status:i.status,highestBid:i.highestBid,currentBid:e,bidCount:i.bidCount}),events:[o,..."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}}buildTerminalTransition(t,i,e,s){return"won"===i?{trackedParticipation:null,displayState:this.buildDisplayState(e,{status:i,highestBid:s.highestBid,currentBid:s.currentBid,bidCount:s.bidCount}),events:[{type:"auction:won",payload:{auctionId:t,winningBid:s.currentBid??"",admissionGrant:s.admissionGrant}}],monitorUpdate:s.admissionGrant?{type:"granted",token:s.admissionGrant}:{type:"ended",outcome:{type:"won",distributionType:"auction",distributionId:t}},stopWatching:!0,autoRebidStatus:null}:{trackedParticipation:null,displayState:this.buildDisplayState(e,{status:i,highestBid:s.highestBid,currentBid:s.currentBid,bidCount:s.bidCount}),events:[{type:"auction:lost",payload:{auctionId:t,highestBid:s.highestBid}}],monitorUpdate:{type:"ended",outcome:{type:"lost"}},stopWatching:!0,autoRebidStatus:null}}buildStatusTransition(t,i,e,{previousParticipation:s,previousStatus:n}){const a=i.currentBid??s?.currentBid,o=i.bidCount??s?.bidCount;if(y(n?.status))return{displayState:null,events:[],monitorUpdate:null,stopWatching:!1,autoRebidStatus:null};if("won"===i.status||"lost"===i.status)return this.buildTerminalTransition(t,i.status,e,{currentBid:a,highestBid:i.highestBid,bidCount:o,admissionGrant:i.admissionGrant});const r=e.closeAt?new Date(e.closeAt).getTime():void 0;if("not_bid"===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 d={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},u=[];let h=null;return n&&(this.checkOutbidConditions(n,i,s)&&(u.push({type:"auction:outbid",payload:{auctionId:t,yourBid:n.currentBid||s?.currentBid||"",highestBid:i.highestBid}}),h=i),"winning"!==n.status&&"winning"===i.status&&u.push({type:"auction:winning",payload:{auctionId:t,amount:a||"",highestBid:i.highestBid}}),n.highestBid!==i.highestBid&&"winning"!==i.status&&u.push({type:"auction:bid-updated",payload:{auctionId:t,highestBid:i.highestBid,bidCount:i.bidCount||0}})),{trackedParticipation:d,displayState:this.buildDisplayState(e,{status:i.status,highestBid:i.highestBid,currentBid:a,bidCount:o}),events:u,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(i,e,n){try{const o=this.lifecycleGeneration;if(!a(e))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 r=this.bumpOperationGeneration(i),d=this.getTrackedParticipation(i);d||(this.logger.warn("Attempting to bid without entering auction first",{auctionId:i}),await this.enter(i,void 0,n),d=this.getTrackedParticipation(i),r=this.bumpOperationGeneration(i)),this.logger.info("Placing bid",{auctionId:i,amount:e});const u=`/auctions/${i}/bid`,h=await this.getAuctionDetailsCached(i),l=await this.http.post(u,{amount:e},n),g=c(t,l,{endpoint:u},{throwOnFailure:!0});if(!g.ok)throw g.error;const p=g.value;p.degraded&&this.logger.warn("Bid committed; the server deferred a post-commit effect to replay",{auctionId:i});const m=await this.proveHighestBid(i,p,e),B=f(p.status),S={status:B,amount:e,highestBid:m,bidCount:p.bidCount??(d?.bidCount||0)+1,admissionGrant:p.admissionGrant,...p.degraded?{degraded:!0}:{}};if(!this.canApplyAsyncResult(i,o,void 0,r))return S;if(this.applyTransition(i,this.buildBidTransition(i,S,e,h,p.admissionGrant,d)),b(S))return S;const v=this.lastStatusCache.get(i);return y(v?.status)||this.lastStatusCache.set(i,{auctionId:i,status:B,currentBid:e,highestBid:m,timeRemaining:0,bidCount:S.bidCount,admissionGrant:p.admissionGrant}),S}catch(o){throw this.logger.error("Failed to place bid",{auctionId:i,amount:e,error:o}),this.events.emit("auction:error",{auctionId:i,error:o}),o}}async proveHighestBid(t,e,s){const n=`/auctions/${t}/highest-bid`,a=d(e.winningBidAmount??e.lastBidAmount??s);try{const t=await this.http.get(n),e=c(i,t,{endpoint:n});return e.ok?d(e.value.amount):a}catch(o){return this.logger.warn("Highest-bid read failed after a committed bid; reporting the committed amount",{auctionId:t,error:o}),a}}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:"not_bid",metadata:i,fingerprint:a.deviceFingerprint}),this.lastStatusCache.delete(t),this.emitTransitionEvents([{type:"auction:entered",payload:{auctionId:t}},{type:"experience:distribution-entered",payload:{distributionType:"auction",distributionId: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||"not_bid"!==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 r=`/auctions/${e}/status`,u=`/auctions/${e}/highest-bid`,[h,l,g]=await Promise.allSettled([this.http.get(r),this.http.get(u),this.getAuctionDetailsCached(e)]);if("rejected"===h.status){if("object"==typeof(o=h.reason)&&null!==o&&"status"in o&&404===o.status)return this.applyMissingStatus(e,s,n,a);throw h.reason}if("rejected"===l.status)throw l.reason;if("rejected"===g.status)throw g.reason;const p=h.value,m=l.value,b=g.value,B=c(t,p,{endpoint:r},{throwOnFailure:!0});if(!B.ok)throw B.error;const S=c(i,m,{endpoint:u},{throwOnFailure:!0});if(!S.ok)throw S.error;const v=B.value,w=S.value,C=this.getAuctionSettleAt(b),A=C?new Date(C).getTime():Number.NaN,I=d(w.amount),R=this.getTrackedParticipation(e),T=v.lastBidAmount??v.winningBidAmount,G={auctionId:e,status:f(v.status),currentBid:R?.currentBid??(T?d(T):void 0),highestBid:I,timeRemaining:Number.isFinite(A)?Math.max(0,A-Date.now()):0,bidCount:v.bidCount,admissionGrant:v.admissionGrant};if(!this.canApplyAsyncResult(e,s,n,a))return G;const M=this.lastStatusCache.get(e),k=this.buildStatusTransition(e,G,b,{previousParticipation:R,previousStatus:M});return this.applyTransition(e,k),this.hasSuccessfulReadThisSession.set(e,!0),Number.isFinite(A)?this.lastSettleAtMs.set(e,A):this.lastSettleAtMs.delete(e),y(M?.status)?this.lastStatusCache.set(e,M):y(G.status)?this.lastStatusCache.set(e,G):k.stopWatching?this.lastStatusCache.delete(e):this.lastStatusCache.set(e,G),this.events.emit("auction:status-updated",{auctionId:e,status:G}),G}catch(o){if(!this.canApplyAsyncResult(e,s,n,a))return this.lastStatusCache.get(e)??{auctionId:e,status:"not_bid",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}var o}applyMissingStatus(t,i,e,s){const n={auctionId:t,status:"not_bid",highestBid:"0.00",timeRemaining:0};if(!this.canApplyAsyncResult(t,i,e,s))return n;const a=this.lastStatusCache.get(t);if(!y(a?.status)){const i=this.buildMissingTransition();this.applyTransition(t,i),this.lastStatusCache.delete(t)}return this.hasSuccessfulReadThisSession.set(t,!0),this.lastSettleAtMs.delete(t),this.events.emit("auction:status-updated",{auctionId:t,status:n}),n}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:d(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);return void 0!==i&&(Date.now()>=i-12e4&&!y(this.lastStatusCache.get(t)?.status))}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(u(i,"0")||u(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=d(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){y(this.lastStatusCache.get(t)?.status)||(this.monitorRuntime.start(t,i,e),this.trackedParticipations.has(t)||this.trackedParticipations.set(t,{auctionId:t,enteredAt:/* @__PURE__ */(new Date).toISOString(),status:"not_bid"}),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
+ 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 d,isMoneyLessThanOrEqualTo as u,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";import{isGrantPendingWin as b,isTerminalAuctionStatus as y,toAuctionConsumerStatus as f}from"./auction-status.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){void 0!==i.trackedParticipation&&(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("outbid"!==i.status)return!1;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)&&"outbid"===i.status}return"outbid"!==t.status&&"outbid"===i.status}buildBidTransition(t,i,e,s,n,a){const o={type:"auction:bid-placed",payload:{auctionId:t,amount:e,status:i.status}};if("won"===i.status&&!b(i)||"lost"===i.status){const a=this.buildTerminalTransition(t,i.status,s,{currentBid:e,highestBid:i.highestBid,bidCount:i.bidCount,admissionGrant:n});return{...a,events:[o,...a.events]}}return{trackedParticipation:{auctionId:t,enteredAt:a?.enteredAt??/* @__PURE__ */(new Date).toISOString(),status:i.status,currentBid:e,highestBid:i.highestBid,bidCount:i.bidCount,lastBidAt:/* @__PURE__ */(new Date).toISOString(),metadata:a?.metadata,fingerprint:a?.fingerprint},displayState:this.buildDisplayState(s,{status:i.status,highestBid:i.highestBid,currentBid:e,bidCount:i.bidCount}),events:[o,..."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}}buildTerminalTransition(t,i,e,s){return"won"===i?{trackedParticipation:null,displayState:this.buildDisplayState(e,{status:i,highestBid:s.highestBid,currentBid:s.currentBid,bidCount:s.bidCount}),events:[{type:"auction:won",payload:{auctionId:t,winningBid:s.currentBid??"",admissionGrant:s.admissionGrant}}],monitorUpdate:s.admissionGrant?{type:"granted",token:s.admissionGrant}:{type:"ended",outcome:{type:"won",distributionType:"auction",distributionId:t}},stopWatching:!0,autoRebidStatus:null}:{trackedParticipation:null,displayState:this.buildDisplayState(e,{status:i,highestBid:s.highestBid,currentBid:s.currentBid,bidCount:s.bidCount}),events:[{type:"auction:lost",payload:{auctionId:t,highestBid:s.highestBid}}],monitorUpdate:{type:"ended",outcome:{type:"lost"}},stopWatching:!0,autoRebidStatus:null}}buildStatusTransition(t,i,e,{previousParticipation:s,previousStatus:n}){const a=i.currentBid??s?.currentBid,o=i.bidCount??s?.bidCount;if(y(n?.status))return{displayState:null,events:[],monitorUpdate:null,stopWatching:!1,autoRebidStatus:null};if("won"===i.status||"lost"===i.status)return this.buildTerminalTransition(t,i.status,e,{currentBid:a,highestBid:i.highestBid,bidCount:o,admissionGrant:i.admissionGrant});const r=e.closeAt?new Date(e.closeAt).getTime():void 0;if("not_bid"===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 d={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},u=[];let h=null;return n&&(this.checkOutbidConditions(n,i,s)&&(u.push({type:"auction:outbid",payload:{auctionId:t,yourBid:n.currentBid||s?.currentBid||"",highestBid:i.highestBid}}),h=i),"winning"!==n.status&&"winning"===i.status&&u.push({type:"auction:winning",payload:{auctionId:t,amount:a||"",highestBid:i.highestBid}}),n.highestBid!==i.highestBid&&"winning"!==i.status&&u.push({type:"auction:bid-updated",payload:{auctionId:t,highestBid:i.highestBid,bidCount:i.bidCount||0}})),{trackedParticipation:d,displayState:this.buildDisplayState(e,{status:i.status,highestBid:i.highestBid,currentBid:a,bidCount:o}),events:u,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(i,e,n,o){try{const r=this.lifecycleGeneration;if(!a(e))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(i),u=this.getTrackedParticipation(i);u||(this.logger.warn("Attempting to bid without entering auction first",{auctionId:i}),await this.enter(i,void 0,n,o),u=this.getTrackedParticipation(i),d=this.bumpOperationGeneration(i)),this.logger.info("Placing bid",{auctionId:i,amount:e});const h=`/auctions/${i}/bid`,l=await this.getAuctionDetailsCached(i),g=await this.http.post(h,{amount:e},n),p=c(t,g,{endpoint:h},{throwOnFailure:!0});if(!p.ok)throw p.error;const m=p.value;m.degraded&&this.logger.warn("Bid committed; the server deferred a post-commit effect to replay",{auctionId:i});const B=await this.proveHighestBid(i,m,e),S=f(m.status),v={status:S,amount:e,highestBid:B,bidCount:m.bidCount??(u?.bidCount||0)+1,admissionGrant:m.admissionGrant,...m.degraded?{degraded:!0}:{}};if(!this.canApplyAsyncResult(i,r,void 0,d))return v;if(this.applyTransition(i,this.buildBidTransition(i,v,e,l,m.admissionGrant,u)),b(v))return v;const w=this.lastStatusCache.get(i);return y(w?.status)||this.lastStatusCache.set(i,{auctionId:i,status:S,currentBid:e,highestBid:B,timeRemaining:0,bidCount:v.bidCount,admissionGrant:m.admissionGrant}),v}catch(r){throw this.logger.error("Failed to place bid",{auctionId:i,amount:e,error:r}),this.events.emit("auction:error",{auctionId:i,error:r}),r}}async proveHighestBid(t,e,s){const n=`/auctions/${t}/highest-bid`,a=d(e.winningBidAmount??e.lastBidAmount??s);try{const t=await this.http.get(n),e=c(i,t,{endpoint:n});return e.ok?d(e.value.amount):a}catch(o){return this.logger.warn("Highest-bid read failed after a committed bid; reporting the committed amount",{auctionId:t,error:o}),a}}async enter(t,i,e,n){try{const a=this.lifecycleGeneration,o=this.store.session;if(!o)throw s.unauthorized("Not authenticated. Call auth.guest() or auth.login() first");const r=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,r))return;this.trackedParticipations.set(t,{auctionId:t,enteredAt:/* @__PURE__ */(new Date).toISOString(),status:"not_bid",metadata:i,fingerprint:o.deviceFingerprint}),this.lastStatusCache.delete(t),this.emitTransitionEvents([{type:"auction:entered",payload:{auctionId:t}},{type:"experience:distribution-entered",payload:{distributionType:"auction",distributionId:t,...n}}])}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){const e=this.getTrackedParticipation(t);if(e&&(void 0!==e.currentBid||"not_bid"!==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 r=`/auctions/${e}/status`,u=`/auctions/${e}/highest-bid`,[h,l,g]=await Promise.allSettled([this.http.get(r),this.http.get(u),this.getAuctionDetailsCached(e)]);if("rejected"===h.status){if("object"==typeof(o=h.reason)&&null!==o&&"status"in o&&404===o.status)return this.applyMissingStatus(e,s,n,a);throw h.reason}if("rejected"===l.status)throw l.reason;if("rejected"===g.status)throw g.reason;const p=h.value,m=l.value,b=g.value,B=c(t,p,{endpoint:r},{throwOnFailure:!0});if(!B.ok)throw B.error;const S=c(i,m,{endpoint:u},{throwOnFailure:!0});if(!S.ok)throw S.error;const v=B.value,w=S.value,C=this.getAuctionSettleAt(b),A=C?new Date(C).getTime():Number.NaN,I=d(w.amount),R=this.getTrackedParticipation(e),T=v.lastBidAmount??v.winningBidAmount,G={auctionId:e,status:f(v.status),currentBid:R?.currentBid??(T?d(T):void 0),highestBid:I,timeRemaining:Number.isFinite(A)?Math.max(0,A-Date.now()):0,bidCount:v.bidCount,admissionGrant:v.admissionGrant};if(!this.canApplyAsyncResult(e,s,n,a))return G;const M=this.lastStatusCache.get(e),k=this.buildStatusTransition(e,G,b,{previousParticipation:R,previousStatus:M});return this.applyTransition(e,k),this.hasSuccessfulReadThisSession.set(e,!0),Number.isFinite(A)?this.lastSettleAtMs.set(e,A):this.lastSettleAtMs.delete(e),y(M?.status)?this.lastStatusCache.set(e,M):y(G.status)?this.lastStatusCache.set(e,G):k.stopWatching?this.lastStatusCache.delete(e):this.lastStatusCache.set(e,G),this.events.emit("auction:status-updated",{auctionId:e,status:G}),G}catch(o){if(!this.canApplyAsyncResult(e,s,n,a))return this.lastStatusCache.get(e)??{auctionId:e,status:"not_bid",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}var o}applyMissingStatus(t,i,e,s){const n={auctionId:t,status:"not_bid",highestBid:"0.00",timeRemaining:0};if(!this.canApplyAsyncResult(t,i,e,s))return n;const a=this.lastStatusCache.get(t);if(!y(a?.status)){const i=this.buildMissingTransition();this.applyTransition(t,i),this.lastStatusCache.delete(t)}return this.hasSuccessfulReadThisSession.set(t,!0),this.lastSettleAtMs.delete(t),this.events.emit("auction:status-updated",{auctionId:t,status:n}),n}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:d(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);return void 0!==i&&(Date.now()>=i-12e4&&!y(this.lastStatusCache.get(t)?.status))}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(u(i,"0")||u(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=d(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){y(this.lastStatusCache.get(t)?.status)||(this.monitorRuntime.start(t,i,e),this.trackedParticipations.has(t)||this.trackedParticipations.set(t,{auctionId:t,enteredAt:/* @__PURE__ */(new Date).toISOString(),status:"not_bid"}),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,9 @@
1
- import { AdmissionGrant, AuctionConsumerStatus, SequenceOutcome } from '@fanfare-io/fanfare-sdk-contracts/consumer-me';
1
+ import { AdmissionGrant, AuctionConsumerStatus, GrantedSettlement, SequenceOutcome } from '@fanfare-io/fanfare-sdk-contracts/consumer-me';
2
+ import { GrantedSelection } from '@fanfare-io/fanfare-sdk-contracts/selection';
2
3
  import { ReadableAtom } from 'nanostores';
3
4
  import { AuctionDisplayState } from '../experiences/distribution-monitor.types';
4
5
  import { DistributionSummary } from '../experiences/journey.types';
6
+ import { CheckoutNextAction, GrantPaymentAffordance } from '../payment/types';
5
7
  /**
6
8
  * Auction participation facts for one consumer.
7
9
  *
@@ -114,6 +116,13 @@ export interface AuctionGranted {
114
116
  consumer: AuctionSequenceConsumerState;
115
117
  /** Admission credential issued by the server for this auction win. */
116
118
  grant: AdmissionGrant;
119
+ /**
120
+ * How this win settles: who consumes the grant, and when money moves for it. The consumer's
121
+ * remaining affordances follow from this and nothing else.
122
+ */
123
+ settlement: GrantedSettlement;
124
+ selection: GrantedSelection;
125
+ pendingAction?: CheckoutNextAction;
117
126
  }
118
127
  /**
119
128
  * The auction journey is terminal for this consumer.
@@ -218,7 +227,7 @@ export interface AuctionSettlingView {
218
227
  /**
219
228
  * Granted auction view rendered for a winning auction participation.
220
229
  */
221
- export interface AuctionGrantedView {
230
+ interface AuctionGrantedViewBase {
222
231
  /** Phase discriminator; `claim()` is legal on this view. */
223
232
  phase: "granted";
224
233
  /** Mechanism discriminator for auction rendering. */
@@ -231,6 +240,35 @@ export interface AuctionGrantedView {
231
240
  consumer: AuctionSequenceConsumerState;
232
241
  /** Admission credential issued by the server for this auction win. */
233
242
  grant: AdmissionGrant;
243
+ /** Product choice carried by the admission register. */
244
+ selection: GrantedSelection;
245
+ }
246
+ /** Granted auction view whose actions follow the server-projected settlement arm. */
247
+ export type AuctionGrantedView = (AuctionGrantedViewBase & {
248
+ /** Fanfare collects payment after this auction grant. */
249
+ settlement: Extract<GrantedSettlement, {
250
+ processor: "internal";
251
+ mode: "at_end";
252
+ }>;
253
+ /** Claim the auction admission credential for checkout. */
254
+ claim(): AdmissionGrant;
255
+ /** Embedded payment actions available before this tab claims the checkout. */
256
+ payment: GrantPaymentAffordance;
257
+ }) | (AuctionGrantedViewBase & {
258
+ /** Fanfare collects payment after this auction grant. */
259
+ settlement: Extract<GrantedSettlement, {
260
+ processor: "internal";
261
+ mode: "at_end";
262
+ }>;
263
+ /** Claim the auction admission credential; this tab has already claimed the checkout. */
264
+ claim(): AdmissionGrant;
265
+ /** Absent after this tab claims the checkout. */
266
+ payment?: undefined;
267
+ }) | (AuctionGrantedViewBase & {
268
+ /** The merchant-owned checkout consumes this auction grant. */
269
+ settlement: Extract<GrantedSettlement, {
270
+ processor: "external";
271
+ }>;
234
272
  /**
235
273
  * Claim the auction admission credential for checkout.
236
274
  *
@@ -239,7 +277,15 @@ export interface AuctionGrantedView {
239
277
  * phase.
240
278
  */
241
279
  claim(): AdmissionGrant;
242
- }
280
+ }) | (AuctionGrantedViewBase & {
281
+ /** Entry already carried the authorization, so no checkout remains after the win. */
282
+ settlement: Extract<GrantedSettlement, {
283
+ processor: "internal";
284
+ mode: "pre_auth";
285
+ }>;
286
+ /** Absent because a pre-authorized win has no checkout to claim against. */
287
+ claim?: undefined;
288
+ });
243
289
  /**
244
290
  * Ended auction view rendered once the auction journey is terminal.
245
291
  */
@@ -253,3 +299,4 @@ export interface AuctionEndedView {
253
299
  /** Server-derived terminal outcome for the auction participation. */
254
300
  outcome: SequenceOutcome;
255
301
  }
302
+ export {};
@@ -1 +1 @@
1
- import{FanfareError as e}from"../core/errors.js";import{getLogger as t}from"../core/logger.js";import{loadRefreshToken as s,clearRefreshToken as r,clearLegacySessionTokens as o,buildAdmissionProofHeaders as i,persistRefreshToken as n}from"../security/admission-proof.js";import{getEventBus as a}from"../state/events.js";import{getSDKStore as h}from"../state/store.js";import{BroadcastChannelTransport as c}from"../sync/broadcast-transport.js";function f(e){const t={};for(const[s,r]of Object.entries(e))"string"==typeof r&&(t[s]=r);return t}class g{constructor(e,s){this.logger=t(),this.store=h(),this.events=a(),this.lastBroadcastTokens=null,this.http=e,this.beaconHttp=s,this.setupTokenChannel(),this.http.updateConfig({...this.http.getConfig(),onUnauthorized:async()=>{await this.handleUnauthorized()}});const r=this.store.beaconToken;r&&this.beaconHttp.updateConfig({...this.beaconHttp.getConfig(),headers:{...this.beaconHttp.getConfig().headers,"X-Beacon-Token":r}})}setupTokenChannel(){const e=new c("fanfare-auth-tokens");e.isAvailable()&&(this.authChannel=e,e.onMessage(e=>{const t=e.data;t&&"string"==typeof t.accessToken&&"string"==typeof t.beaconToken&&"string"==typeof t.refreshToken&&(this.lastBroadcastTokens={accessToken:t.accessToken,beaconToken:t.beaconToken,refreshToken:t.refreshToken})}))}broadcastSessionTokens(e){this.authChannel?.send(e)}check(){const e=this.store.session;return e?{isAuthenticated:!0,session:e,isGuest:"guest"===e.type,identityExpiresAt:e.identityExpiresAt}:{isAuthenticated:!1}}async guest(){try{this.logger.info("Creating guest session");const e=await this.sessionMintProofHeaders("/auth/guest"),{session:t,accessToken:s,refreshToken:r,beaconToken:o,refreshTokenTtlSeconds:i}=await this.http.post("/auth/guest",void 0,{headers:e});return this.attachIdentityExpiresAt(t,i),this.store.setSession(t),await this.setRefreshTokenDurable(r??null),this.store.setBeaconToken(o),this.http.updateConfig({...this.http.getConfig(),headers:{...this.http.getConfig().headers,Authorization:`Bearer ${s}`}}),this.beaconHttp.updateConfig({...this.beaconHttp.getConfig(),headers:{...this.beaconHttp.getConfig().headers,"X-Beacon-Token":o}}),this.scheduleTokenRefresh(t.expiresAt),this.events.emit("auth:authenticated",{session:t,isNew:!0}),t}catch(e){const t=e instanceof Error?e:new Error(String(e));throw this.logger.error("Failed to create guest session",t),this.events.emit("auth:error",{error:t,context:"guest"}),t}}async requestOtp(e){try{this.logger.info("Requesting OTP");const t="string"==typeof e?{email:e}:e;await this.http.post("/auth/otp/request",t)}catch(t){const e=t instanceof Error?t:new Error(String(t));throw this.logger.error("Failed to request OTP",e),this.events.emit("auth:error",{error:e,context:"requestOtp"}),e}}async verifyOtp(e){try{const t=e;this.logger.info("Verifying OTP",{email:t.email,phone:t.phone});const s=await this.sessionMintProofHeaders("/auth/otp/verify"),{session:r,accessToken:o,refreshToken:i,beaconToken:n,refreshTokenTtlSeconds:a}=await this.http.post("/auth/otp/verify",t,{headers:s});return this.attachIdentityExpiresAt(r,a),this.store.setSession(r),await this.setRefreshTokenDurable(i??null),this.store.setBeaconToken(n),this.http.updateConfig({...this.http.getConfig(),headers:{...this.http.getConfig().headers,Authorization:`Bearer ${o}`}}),this.beaconHttp.updateConfig({...this.beaconHttp.getConfig(),headers:{...this.beaconHttp.getConfig().headers,"X-Beacon-Token":n}}),this.scheduleTokenRefresh(r.expiresAt),this.events.emit("auth:authenticated",{session:r,isNew:!0}),r}catch(t){const e=t instanceof Error?t:new Error(String(t));throw this.logger.error("Failed to verify OTP",e),this.events.emit("auth:error",{error:e,context:"verifyOtp"}),e}}async exchangeExternal(e){try{const t="string"==typeof e?{exchangeCode:e}:e;this.logger.info("Exchanging external auth code");const s=await this.sessionMintProofHeaders("/auth/external/exchange"),{session:r,accessToken:o,refreshToken:i,beaconToken:n,refreshTokenTtlSeconds:a}=await this.http.post("/auth/external/exchange",t,{headers:s});return this.attachIdentityExpiresAt(r,a),this.store.setSession(r),await this.setRefreshTokenDurable(i??null),this.store.setBeaconToken(n),this.http.updateConfig({...this.http.getConfig(),headers:{...this.http.getConfig().headers,Authorization:`Bearer ${o}`}}),this.beaconHttp.updateConfig({...this.beaconHttp.getConfig(),headers:{...this.beaconHttp.getConfig().headers,"X-Beacon-Token":n}}),this.scheduleTokenRefresh(r.expiresAt),this.events.emit("auth:authenticated",{session:r,isNew:!0}),r}catch(t){const e=t instanceof Error?t:new Error(String(t));throw this.logger.error("Failed to exchange external auth code",e),this.events.emit("auth:error",{error:e,context:"exchangeExternal"}),e}}async login(e){await this.requestOtp({email:e.email})}async logout(){try{if(!this.store.session)return;const e=this.store.refreshToken??await s();await this.http.post("/auth/logout",e?{refreshToken:e}:{},{skipUnauthorizedHandler:!0}).catch(()=>{}),await this.clearAuthState(),this.events.emit("auth:logout",{reason:"user"})}catch(e){const t=e instanceof Error?e:new Error(String(e));throw this.logger.error("Error during logout",t),this.store.clearAll(),this.lastBroadcastTokens=null,await r(),t}}async clearAuthState(){this.cancelTokenRefresh(),this.store.clearAll(),this.lastBroadcastTokens=null,await r();const e=this.http.getConfig(),{Authorization:t,...s}=e.headers||{};this.http.updateConfig({...e,headers:s});const o=this.beaconHttp.getConfig(),{"X-Beacon-Token":i,...n}=o.headers||{};this.beaconHttp.updateConfig({...o,headers:n})}getSession(){return this.store.session}async rehydrate(){const e=await s();if(o(),!e)return;if(this.store.setRefreshToken(e),!this.hasBearer()){try{await this.refresh()}catch(r){this.logger.debug("Eager rehydrate refresh failed; falling back to reactive 401",{error:r instanceof Error?r.message:String(r)})}return}const t=this.store.session;t?.expiresAt&&this.scheduleTokenRefresh(t.expiresAt)}hasBearer(){return Boolean(this.http.getConfig().headers?.Authorization)}async refresh(){if("undefined"!=typeof navigator&&"locks"in navigator){const e=await s(),t=e??this.store.refreshToken;return navigator.locks.request("fanfare-refresh",()=>this._doRefresh(t,e))}return this.refreshPromise?(this.logger.debug("Refresh already in progress, returning existing promise"),this.refreshPromise):(this.refreshPromise=this._doRefresh().finally(()=>{this.refreshPromise=void 0}),this.refreshPromise)}absoluteAuthUrl(e){return new URL(e,this.http.getConfig().baseUrl).toString()}async sessionMintProofHeaders(e){const t=await i({url:this.absoluteAuthUrl(e),method:"POST"});return t?f(t):{}}computeIdentityExpiresAt(e){if("number"==typeof e&&Number.isFinite(e))return new Date(Date.now()+1e3*e).toISOString()}attachIdentityExpiresAt(e,t){const s=this.computeIdentityExpiresAt(t);s&&(e.identityExpiresAt=s)}async setRefreshTokenDurable(e){this.store.setRefreshToken(e),e?await n(e):await r()}applySessionTokens(e){this.store.setBeaconToken(e.beaconToken),this.http.updateConfig({...this.http.getConfig(),headers:{...this.http.getConfig().headers,Authorization:`Bearer ${e.accessToken}`}}),this.beaconHttp.updateConfig({...this.beaconHttp.getConfig(),headers:{...this.beaconHttp.getConfig().headers,"X-Beacon-Token":e.beaconToken}});const t=this.store.session;t?.expiresAt&&this.scheduleTokenRefresh(t.expiresAt),this.store.session&&this.events.emit("auth:refreshed",{session:this.store.session})}async _doRefresh(t,r){const o=await s(),n=o??this.store.refreshToken;if(null!==o&&o!==this.store.refreshToken&&this.store.setRefreshToken(o),!n)throw this.logger.info("No refresh token available; clearing auth state"),await this.clearAuthState(),this.events.emit("auth:logout",{reason:"expired"}),new Error("No refresh token available");if(r&&null===o)throw this.logger.debug("Durable refresh token cleared by a concurrent caller; skipping re-POST"),await this.clearAuthState(),this.events.emit("auth:logout",{reason:"expired"}),new Error("Refresh token cleared by a concurrent caller");if(void 0!==t&&null!==o&&o!==t){this.logger.debug("Refresh already completed by a concurrent caller; adopting rotated session");const e=this.lastBroadcastTokens;return void(e&&e.refreshToken===o?this.applySessionTokens(e):this.store.session&&this.events.emit("auth:refreshed",{session:this.store.session}))}const a=await i({url:this.absoluteAuthUrl("/auth/refresh"),method:"POST"});if(!a)throw this.logger.info("No DPoP proof available for refresh; clearing auth state"),await this.clearAuthState(),this.events.emit("auth:logout",{reason:"expired"}),new Error("DPoP proof unavailable for refresh");try{this.logger.info("Refreshing access token");const{accessToken:e,refreshToken:t,beaconToken:s,accessTokenExpiresAt:r,refreshTokenTtlSeconds:o}=await this.http.post("/auth/refresh",{refreshToken:n},{headers:f(a),skipUnauthorizedHandler:!0});if(await this.setRefreshTokenDurable(t??null),t&&this.broadcastSessionTokens({accessToken:e,beaconToken:s,refreshToken:t}),r){const e=this.computeIdentityExpiresAt(o);e?this.store.updateSessionExpiry(r,e):this.store.updateSessionExpiry(r)}this.applySessionTokens({accessToken:e,beaconToken:s})}catch(h){const t=h instanceof Error?h:new Error(String(h)),s=h instanceof e?h.status:void 0;throw 401===s?(this.logger.error("Refresh rejected as unauthorized; clearing auth state",t),await this.clearAuthState(),this.events.emit("auth:logout",{reason:"expired"})):this.logger.warn("Refresh failed transiently; preserving durable refresh token",{status:s,error:t.message}),t}}async handleUnauthorized(){this.logger.warn("Received 401, attempting to refresh token");try{await this.refresh()}catch(e){const t=e instanceof Error?e:new Error(String(e));this.logger.error("Failed to handle 401",t),this.events.emit("auth:error",{error:new Error("Session expired"),context:"unauthorized"})}}scheduleTokenRefresh(e){if(this.cancelTokenRefresh(),!this.store.refreshToken)return;const t=Date.now(),s=new Date(e).getTime(),r=Number.isFinite(s),o=r?Math.max(s-3e5-t,3e4):3e4;this.logger.debug("Scheduling token refresh",{expiresAt:r?new Date(s).toISOString():"invalid",delayMs:o}),this.refreshTimer=setTimeout(()=>{this.refresh().catch(e=>{this.logger.error("Scheduled token refresh failed",e)})},o)}cancelTokenRefresh(){this.refreshTimer&&(clearTimeout(this.refreshTimer),this.refreshTimer=void 0,this.logger.debug("Cancelled token refresh timer"))}destroy(){this.cancelTokenRefresh(),this.refreshPromise=void 0,this.authChannel?.close(),this.authChannel=void 0}}export{g as AuthenticationModule};
1
+ import*as e from"valibot";import{FanfareError as t}from"../core/errors.js";import{getLogger as s}from"../core/logger.js";import{parseResponse as r}from"../core/parse-response.js";import{loadRefreshToken as o,clearRefreshToken as i,clearLegacySessionTokens as n,buildAdmissionProofHeaders as a,persistRefreshToken as h}from"../security/admission-proof.js";import{getEventBus as c}from"../state/events.js";import{getSDKStore as g}from"../state/store.js";import{BroadcastChannelTransport as f}from"../sync/broadcast-transport.js";const l=e.object({accessToken:e.pipe(e.string(),e.minLength(1)),refreshToken:e.optional(e.pipe(e.string(),e.minLength(1))),beaconToken:e.pipe(e.string(),e.minLength(1)),accessTokenExpiresAt:e.optional(e.string()),refreshTokenTtlSeconds:e.optional(e.number())});function u(e){const t={};for(const[s,r]of Object.entries(e))"string"==typeof r&&(t[s]=r);return t}class p{constructor(e,t){this.logger=s(),this.store=g(),this.events=c(),this.lastBroadcastTokens=null,this.http=e,this.beaconHttp=t,this.setupTokenChannel(),this.http.updateConfig({...this.http.getConfig(),onUnauthorized:async()=>{await this.handleUnauthorized()}});const r=this.store.beaconToken;r&&this.beaconHttp.updateConfig({...this.beaconHttp.getConfig(),headers:{...this.beaconHttp.getConfig().headers,"X-Beacon-Token":r}})}setupTokenChannel(){const e=new f("fanfare-auth-tokens");e.isAvailable()&&(this.authChannel=e,e.onMessage(e=>{const t=e.data;t&&"string"==typeof t.accessToken&&"string"==typeof t.beaconToken&&"string"==typeof t.refreshToken&&(this.lastBroadcastTokens={accessToken:t.accessToken,beaconToken:t.beaconToken,refreshToken:t.refreshToken})}))}broadcastSessionTokens(e){this.authChannel?.send(e)}check(){const e=this.store.session;return e?{isAuthenticated:!0,session:e,isGuest:"guest"===e.type,identityExpiresAt:e.identityExpiresAt}:{isAuthenticated:!1}}async guest(){try{this.logger.info("Creating guest session");const e=await this.sessionMintProofHeaders("/auth/guest"),{session:t,accessToken:s,refreshToken:r,beaconToken:o,refreshTokenTtlSeconds:i}=await this.http.post("/auth/guest",void 0,{headers:e});return this.attachIdentityExpiresAt(t,i),this.store.setSession(t),await this.setRefreshTokenDurable(r??null),this.store.setBeaconToken(o),this.http.updateConfig({...this.http.getConfig(),headers:{...this.http.getConfig().headers,Authorization:`Bearer ${s}`}}),this.beaconHttp.updateConfig({...this.beaconHttp.getConfig(),headers:{...this.beaconHttp.getConfig().headers,"X-Beacon-Token":o}}),this.scheduleTokenRefresh(t.expiresAt),this.events.emit("auth:authenticated",{session:t,isNew:!0}),t}catch(e){const t=e instanceof Error?e:new Error(String(e));throw this.logger.error("Failed to create guest session",t),this.events.emit("auth:error",{error:t,context:"guest"}),t}}async requestOtp(e){try{this.logger.info("Requesting OTP");const t="string"==typeof e?{email:e}:e;await this.http.post("/auth/otp/request",t)}catch(t){const e=t instanceof Error?t:new Error(String(t));throw this.logger.error("Failed to request OTP",e),this.events.emit("auth:error",{error:e,context:"requestOtp"}),e}}async verifyOtp(e){try{const t=e;this.logger.info("Verifying OTP",{email:t.email,phone:t.phone});const s=await this.sessionMintProofHeaders("/auth/otp/verify"),{session:r,accessToken:o,refreshToken:i,beaconToken:n,refreshTokenTtlSeconds:a}=await this.http.post("/auth/otp/verify",t,{headers:s});return this.attachIdentityExpiresAt(r,a),this.store.setSession(r),await this.setRefreshTokenDurable(i??null),this.store.setBeaconToken(n),this.http.updateConfig({...this.http.getConfig(),headers:{...this.http.getConfig().headers,Authorization:`Bearer ${o}`}}),this.beaconHttp.updateConfig({...this.beaconHttp.getConfig(),headers:{...this.beaconHttp.getConfig().headers,"X-Beacon-Token":n}}),this.scheduleTokenRefresh(r.expiresAt),this.events.emit("auth:authenticated",{session:r,isNew:!0}),r}catch(t){const e=t instanceof Error?t:new Error(String(t));throw this.logger.error("Failed to verify OTP",e),this.events.emit("auth:error",{error:e,context:"verifyOtp"}),e}}async exchangeExternal(e){try{const t="string"==typeof e?{exchangeCode:e}:e;this.logger.info("Exchanging external auth code");const s=await this.sessionMintProofHeaders("/auth/external/exchange"),{session:r,accessToken:o,refreshToken:i,beaconToken:n,refreshTokenTtlSeconds:a}=await this.http.post("/auth/external/exchange",t,{headers:s});return this.attachIdentityExpiresAt(r,a),this.store.setSession(r),await this.setRefreshTokenDurable(i??null),this.store.setBeaconToken(n),this.http.updateConfig({...this.http.getConfig(),headers:{...this.http.getConfig().headers,Authorization:`Bearer ${o}`}}),this.beaconHttp.updateConfig({...this.beaconHttp.getConfig(),headers:{...this.beaconHttp.getConfig().headers,"X-Beacon-Token":n}}),this.scheduleTokenRefresh(r.expiresAt),this.events.emit("auth:authenticated",{session:r,isNew:!0}),r}catch(t){const e=t instanceof Error?t:new Error(String(t));throw this.logger.error("Failed to exchange external auth code",e),this.events.emit("auth:error",{error:e,context:"exchangeExternal"}),e}}async login(e){await this.requestOtp({email:e.email})}async logout(){try{if(!this.store.session)return;const e=this.store.refreshToken??await o();await this.http.post("/auth/logout",e?{refreshToken:e}:{},{skipUnauthorizedHandler:!0}).catch(()=>{}),await this.clearAuthState(),this.events.emit("auth:logout",{reason:"user"})}catch(e){const t=e instanceof Error?e:new Error(String(e));throw this.logger.error("Error during logout",t),this.store.clearAll(),this.lastBroadcastTokens=null,await i(),t}}async clearAuthState(){this.cancelTokenRefresh(),this.store.clearAll(),this.lastBroadcastTokens=null,await i();const e=this.http.getConfig(),{Authorization:t,...s}=e.headers||{};this.http.updateConfig({...e,headers:s});const r=this.beaconHttp.getConfig(),{"X-Beacon-Token":o,...n}=r.headers||{};this.beaconHttp.updateConfig({...r,headers:n})}getSession(){return this.store.session}async rehydrate(){const e=await o();if(n(),!e)return;if(this.store.setRefreshToken(e),!this.hasBearer()){try{await this.refresh()}catch(s){this.logger.debug("Eager rehydrate refresh failed; falling back to reactive 401",{error:s instanceof Error?s.message:String(s)})}return}const t=this.store.session;t?.expiresAt&&this.scheduleTokenRefresh(t.expiresAt)}hasBearer(){return Boolean(this.http.getConfig().headers?.Authorization)}async refresh(){if("undefined"!=typeof navigator&&"locks"in navigator){const e=await o(),t=e??this.store.refreshToken;return navigator.locks.request("fanfare-refresh",()=>this._doRefresh(t,e))}return this.refreshPromise?(this.logger.debug("Refresh already in progress, returning existing promise"),this.refreshPromise):(this.refreshPromise=this._doRefresh().finally(()=>{this.refreshPromise=void 0}),this.refreshPromise)}absoluteAuthUrl(e){return new URL(e,this.http.getConfig().baseUrl).toString()}async sessionMintProofHeaders(e){const t=await a({url:this.absoluteAuthUrl(e),method:"POST"});return t?u(t):{}}computeIdentityExpiresAt(e){if("number"==typeof e&&Number.isFinite(e))return new Date(Date.now()+1e3*e).toISOString()}attachIdentityExpiresAt(e,t){const s=this.computeIdentityExpiresAt(t);s&&(e.identityExpiresAt=s)}async setRefreshTokenDurable(e){this.store.setRefreshToken(e),e?await h(e):await i()}applySessionTokens(e){this.store.setBeaconToken(e.beaconToken),this.http.updateConfig({...this.http.getConfig(),headers:{...this.http.getConfig().headers,Authorization:`Bearer ${e.accessToken}`}}),this.beaconHttp.updateConfig({...this.beaconHttp.getConfig(),headers:{...this.beaconHttp.getConfig().headers,"X-Beacon-Token":e.beaconToken}});const t=this.store.session;t?.expiresAt&&this.scheduleTokenRefresh(t.expiresAt),this.store.session&&this.events.emit("auth:refreshed",{session:this.store.session})}async _doRefresh(e,s){const i=await o(),n=i??this.store.refreshToken;if(null!==i&&i!==this.store.refreshToken&&this.store.setRefreshToken(i),!n)throw this.logger.info("No refresh token available; clearing auth state"),await this.clearAuthState(),this.events.emit("auth:logout",{reason:"expired"}),new Error("No refresh token available");if(s&&null===i)throw this.logger.debug("Durable refresh token cleared by a concurrent caller; skipping re-POST"),await this.clearAuthState(),this.events.emit("auth:logout",{reason:"expired"}),new Error("Refresh token cleared by a concurrent caller");if(void 0!==e&&null!==i&&i!==e){this.logger.debug("Refresh already completed by a concurrent caller; adopting rotated session");const e=this.lastBroadcastTokens;return void(e&&e.refreshToken===i?this.applySessionTokens(e):this.store.session&&this.events.emit("auth:refreshed",{session:this.store.session}))}const h=await a({url:this.absoluteAuthUrl("/auth/refresh"),method:"POST"});if(!h)throw this.logger.info("No DPoP proof available for refresh; clearing auth state"),await this.clearAuthState(),this.events.emit("auth:logout",{reason:"expired"}),new Error("DPoP proof unavailable for refresh");try{this.logger.info("Refreshing access token");const e=await this.http.post("/auth/refresh",{refreshToken:n},{headers:u(h),skipUnauthorizedHandler:!0}),t=r(l,e,{endpoint:"/auth/refresh"},{throwOnFailure:!0});if(!t.ok)throw t.error;const{accessToken:s,refreshToken:o,beaconToken:i,accessTokenExpiresAt:a,refreshTokenTtlSeconds:c}=t.value;if(await this.setRefreshTokenDurable(o??null),o&&this.broadcastSessionTokens({accessToken:s,beaconToken:i,refreshToken:o}),a){const e=this.computeIdentityExpiresAt(c);e?this.store.updateSessionExpiry(a,e):this.store.updateSessionExpiry(a)}this.applySessionTokens({accessToken:s,beaconToken:i})}catch(c){const e=c instanceof Error?c:new Error(String(c)),s=c instanceof t?c.status:void 0;throw 401===s?(this.logger.error("Refresh rejected as unauthorized; clearing auth state",e),await this.clearAuthState(),this.events.emit("auth:logout",{reason:"expired"})):this.logger.warn("Refresh failed transiently; preserving durable refresh token",{status:s,error:e.message}),e}}async handleUnauthorized(){this.logger.warn("Received 401, attempting to refresh token");try{await this.refresh()}catch(e){const t=e instanceof Error?e:new Error(String(e));this.logger.error("Failed to handle 401",t),this.events.emit("auth:error",{error:new Error("Session expired"),context:"unauthorized"})}}scheduleTokenRefresh(e){if(this.cancelTokenRefresh(),!this.store.refreshToken)return;const t=Date.now(),s=new Date(e).getTime(),r=Number.isFinite(s),o=r?Math.max(s-3e5-t,3e4):3e4;this.logger.debug("Scheduling token refresh",{expiresAt:r?new Date(s).toISOString():"invalid",delayMs:o}),this.refreshTimer=setTimeout(()=>{this.refresh().catch(e=>{this.logger.error("Scheduled token refresh failed",e)})},o)}cancelTokenRefresh(){this.refreshTimer&&(clearTimeout(this.refreshTimer),this.refreshTimer=void 0,this.logger.debug("Cancelled token refresh timer"))}destroy(){this.cancelTokenRefresh(),this.refreshPromise=void 0,this.authChannel?.close(),this.authChannel=void 0}}export{p as AuthenticationModule};
@@ -1 +1 @@
1
- import{FanfareError as e,ErrorCodes as t}from"../core/errors.js";import{getLogger as r}from"../core/logger.js";import{getEventBus as n}from"../state/events.js";import{getSDKStore as s}from"../state/store.js";import{EventBatcher as o}from"./batching.js";import{enrichEvent as i}from"./enrichment.js";import{validateEvent as c}from"./validation.js";class a{constructor(e,t={},i){this.logger=r(),this.events=n(),this.store=s(),this.http=e,this.config=t,this.resolveSequenceId=i,this.batcher=new o(t,async e=>{await this.sendBatch(e)}),this.logger.debug("Beacon tracking module initialized",{config:t})}bind(r){const n="string"==typeof r?r.trim():"";if(""===n)throw new e("beacon.bind requires a non-empty experienceId.",t.VALIDATION_ERROR);const s=e=>{const{experienceId:t,sequenceId:r,...s}=e,o=this.resolveSequenceId?.(n);return{...s,experienceId:n,...void 0!==o?{sequenceId:o}:{}}};return{track:e=>this.track(s(e)),trackBatch:e=>this.trackBatch(e.map(s))}}async track(e){try{const t=i(e,this.config);c(t),this.batcher.add(t),this.events.emit("beacon:queued",{event:t}),this.logger.debug("Event queued for tracking",{event:t})}catch(t){throw this.logger.error("Failed to queue event",{error:t,event:e}),this.events.emit("beacon:error",{error:t,event:e}),t}}async trackBatch(e){try{const t=e.map(e=>{const t=i(e,this.config);return c(t),t}),r=await this.sendBatch(t);this.logger[r?"info":"warn"](r?"Batch tracked successfully":"Batch queued but send failed",{count:t.length})}catch(t){throw this.logger.error("Failed to track batch",{error:t,count:e.length}),this.events.emit("beacon:error",{error:t,events:e}),t}}async flush(){await this.batcher.flush()}async sendBatch(e){if(0===e.length)return!0;try{const t=1===e.length?"/events":"/events/batch",r=1===e.length?e[0]:e,n=this.http.getConfig().headers?.["X-Organization-Id"],s="string"==typeof n?n:void 0,o=this.store.session?.consumerId,i=e=>({...e,...s?{organizationId:s}:{},...o?{consumerId:o}:{}}),c=Array.isArray(r)?r.map(i):i(r);return await this.http.post(t,c),this.events.emit("beacon:sent",{count:e.length}),this.logger.info("Events sent to beacon",{count:e.length}),!0}catch(t){return this.logger.error("Failed to send events",{error:t,count:e.length}),this.events.emit("beacon:error",{error:t,events:e}),!1}}destroy(){this.batcher.destroy()}}export{a as BeaconTrackingModule};
1
+ import{FanfareError as e,ErrorCodes as t}from"../core/errors.js";import{getLogger as r}from"../core/logger.js";import{getEventBus as n}from"../state/events.js";import{getSDKStore as s}from"../state/store.js";import{EventBatcher as o}from"./batching.js";import{enrichEvent as i}from"./enrichment.js";import{validateEvent as c}from"./validation.js";class a{constructor(e,t={},i){this.logger=r(),this.events=n(),this.store=s(),this.http=e,this.config=t,this.resolveSequenceId=i,this.batcher=new o(t,async e=>{await this.sendBatch(e)}),this.logger.debug("Beacon tracking module initialized",{config:t})}bind(r){const n="string"==typeof r?r.trim():"";if(""===n)throw new e("beacon.bind requires a non-empty experienceId.",t.VALIDATION_ERROR);const s=e=>{const{experienceId:t,sequenceId:r,...s}=e,o=this.resolveSequenceId?.(n);return{...s,experienceId:n,...void 0!==o?{sequenceId:o}:{}}};return{track:e=>this.track(s(e)),trackBatch:e=>this.trackBatch(e.map(s))}}async track(e){try{const t=i(e,this.config);c(t),this.batcher.add(t),this.events.emit("beacon:queued",{event:t}),this.logger.debug("Event queued for tracking",{eventName:t.eventName,eventId:t.eventId})}catch(t){throw this.logger.error("Failed to queue event",{error:t,eventName:e.eventName}),this.events.emit("beacon:error",{error:t,event:e}),t}}async trackBatch(e){try{const t=e.map(e=>{const t=i(e,this.config);return c(t),t}),r=await this.sendBatch(t);this.logger[r?"info":"warn"](r?"Batch tracked successfully":"Batch queued but send failed",{count:t.length})}catch(t){throw this.logger.error("Failed to track batch",{error:t,count:e.length}),this.events.emit("beacon:error",{error:t,events:e}),t}}async flush(){await this.batcher.flush()}async sendBatch(e){if(0===e.length)return!0;try{const t=1===e.length?"/events":"/events/batch",r=1===e.length?e[0]:e,n=this.http.getConfig().headers?.["X-Organization-Id"],s="string"==typeof n?n:void 0,o=this.store.session?.consumerId,i=e=>({...e,...s?{organizationId:s}:{},...o?{consumerId:o}:{}}),c=Array.isArray(r)?r.map(i):i(r);return await this.http.post(t,c),this.events.emit("beacon:sent",{count:e.length}),this.logger.info("Events sent to beacon",{count:e.length}),!0}catch(t){return this.logger.error("Failed to send events",{error:t,count:e.length}),this.events.emit("beacon:error",{error:t,events:e}),!1}}destroy(){this.batcher.destroy()}}export{a as BeaconTrackingModule};
@@ -84,7 +84,7 @@ export interface BeaconModule {
84
84
  * await fanfare.beacon.track({
85
85
  * eventName: "pageView",
86
86
  * eventProperties: {
87
- * pageType: "home"
87
+ * pageType: "hosted"
88
88
  * }
89
89
  * });
90
90
  * ```
@@ -166,4 +166,11 @@ export interface BeaconConfig {
166
166
  * @default false
167
167
  */
168
168
  disableMarketingParams?: boolean;
169
+ /**
170
+ * Disable the built-in lifecycle adapter
171
+ * When true, no experience or sequence lifecycle event is forwarded to beacon.
172
+ * Merchant subscribers on the SDK event bus are unaffected.
173
+ * @default false
174
+ */
175
+ disableLifecycleEvents?: boolean;
169
176
  }
@@ -1,2 +1,2 @@
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{PaymentMethodManagementModule as l}from"../payment/payment.module.js";import{ReservationManagementModule as u}from"../payment/reservations.module.js";import{QueueManagementModule as c}from"../queues/queue.module.js";import{resolveCapabilityOptions as g}from"../state/capability-token-registry.js";import{getEventBus as y}from"../state/events.js";import{getSDKStore as m}from"../state/store.js";import{CrossTabCoordinator as p}from"../sync/cross-tab-coordinator.js";import{TimedReleaseManagementModule as f}from"../timed-releases/timed-release.module.js";import{version as b}from"../version.js";import{WaitlistManagementModule as w}from"../waitlists/waitlist.module.js";import{FanfareError as h,ErrorCodes as v}from"./errors.js";import{createHttpClient as I}from"./http.js";import{createLogger as M}from"./logger.js";let S=null;async function j(j){const A=new o(j),k=A.get(),x=M({enabled:k.debug||!1,level:k.debug?"debug":"warn",prefix:"Fanfare"});if(x.info("Initializing Fanfare SDK",{organizationId:j.organizationId,environment:k.environment}),S)throw new h("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.",v.ALREADY_INITIALIZED);const D=/* @__PURE__ */Symbol("fanfare-sdk"),E=I({baseUrl:A.apiUrl,credentials:k.credentials,headers:{"X-Organization-Id":j.organizationId,"X-Publishable-Key":j.publishableKey,"X-Fanfare-API-Version":"2025-01-15","X-Fanfare-SDK-Version":b,"X-Fanfare-Client-Type":"browser"},timeout:3e4,retryConfig:{maxRetries:3,delayTimer:1e3,retryOnNetworkError:!0}},k),q=I({baseUrl:A.beaconUrl,credentials:"omit",headers:{"X-Organization-Id":j.organizationId,"X-Publishable-Key":j.publishableKey,"X-Fanfare-API-Version":"2025-01-15","X-Fanfare-SDK-Version":b,"X-Fanfare-Client-Type":"browser"},timeout:3e4,retryConfig:{maxRetries:3,delayTimer:1e3,retryOnNetworkError:!0}},k),R=new r(E,q),C=new i(E),z=new c(E),O=new s(E),F=new t(E),K=new e(E),X=new w(E),P=new f(E),T=new a(E,z,O,F,X,P),$=new l(E),J=new u(E),N=y(),V=m();N.on("auth:logout",()=>$.destroy());const B=/* @__PURE__ */new Map,U=/* @__PURE__ */new Map,L=new n(q,k.beacon||{},e=>{const t=U.get(e)?.snapshot$.get();return"routed"===t?.journeyStage?t.sequenceId:void 0}),G=/* @__PURE__ */new Map;let H,_;const Y=e=>{const t=U.get(e);if(t)return x.debug("Returning existing journey handle",{experienceId:e}),t;x.info("Creating new journey",{experienceId:e});const r=new d(e,H);B.set(e,r),_?.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(),_?.unregisterJourney(e),B.delete(e),U.delete(e)}};return U.set(e,n),n},Z=async e=>{const t=e??await T.getMe(),r=/* @__PURE__ */new Set,n=new Set(t.active.journeys.map(e=>e.experienceId));for(const i of n)Y(i);for(const i of n){const e=B.get(i);e&&await e.resumeFromMe(t)&&r.add(i)}return[...r]},Q=async e=>{const t=/* @__PURE__ */new Set;for(const r of Object.keys(V.activeJourneys))Y(r),t.add(r);if(!e&&!R.getSession())return[...t];for(const r of await Z(e))t.add(r);return[...t]},W={check:()=>R.check(),guest:()=>R.guest(),requestOtp:e=>R.requestOtp(e),verifyOtp:e=>R.verifyOtp(e),exchangeExternal:e=>R.exchangeExternal(e),login:e=>R.login(e),logout:()=>R.logout(),getSession:()=>R.getSession(),refresh:()=>R.refresh()},ee={initiate:e=>C.initiate(e),verify:e=>C.verify(e)},te={get:e=>Y(e),list:()=>Array.from(
2
- /* @__PURE__ */new Set([...U.keys(),...B.keys(),...Object.keys(V.activeJourneys)])),resumeAll:e=>Q(e)},re={bind:e=>L.bind(e),track:e=>L.track(e),trackBatch:e=>L.trackBatch(e),flush:()=>L.flush()},ne=(e,t)=>{if(G.has(e.id))x.warn("CDP adapter already registered, skipping",{adapterId:e.id});else{try{e.init(N,t)}catch(r){return void x.error("CDP adapter failed to initialize",{adapterId:e.id,error:r})}G.set(e.id,e),x.info("CDP adapter registered",{adapterId:e.id})}},ie=(e,t)=>N.on(e,t),oe=async()=>{x.info("Destroying SDK instance");for(const t of G.values())try{t.destroy()}catch(e){x.error("CDP adapter failed to destroy",{adapterId:t.id,error:e})}G.clear();for(const t of Array.from(U.values()))try{t.destroy()}catch(e){x.error("Journey handle failed to destroy",{error:e})}B.clear(),U.clear(),"destroy"in R&&"function"==typeof R.destroy&&R.destroy(),z.destroy(),O.destroy(),F.destroy(),K.destroy(),X.destroy(),P.destroy(),T.destroy(),L.destroy(),$.destroy(),_?.close(),N.off(),S===D&&(S=null)};H={auth:W,challenges:ee,journeys:te,queues:{get:e=>z.get(e),enter:(e,t,r,n)=>z.enter(e,t,g({distributionId:e},r),n),leave:(e,t)=>z.leave(e,g({distributionId:e},t)),status:e=>z.status(e),startMonitoring:(e,t,r)=>z.startMonitoring(e,t,r),stopMonitoring:e=>z.stopMonitoring(e),isMonitoring:e=>z.isMonitoring(e)},draws:{get:e=>O.get(e),enter:(e,t,r,n)=>O.enter(e,t,g({distributionId:e},r),n),leave:(e,t)=>O.leave(e,g({distributionId:e},t)),status:e=>O.status(e),startMonitoring:(e,t,r)=>O.startMonitoring(e,t,r),stopMonitoring:e=>O.stopMonitoring(e),isMonitoring:e=>O.isMonitoring(e)},auctions:{get:e=>F.get(e),bid:(e,t,r)=>F.bid(e,t,g({distributionId:e},r)),enter:(e,t,r)=>F.enter(e,t,g({distributionId:e},r)),leave:(e,t)=>F.leave(e,g({distributionId:e},t)),status:e=>F.status(e),getBidHistory:e=>F.getBidHistory(e),enableAutoRebid:(e,t,r)=>F.enableAutoRebid(e,t,r),disableAutoRebid:e=>F.disableAutoRebid(e),getAutoRebidConfig:e=>F.getAutoRebidConfig(e),startMonitoring:(e,t,r)=>F.startMonitoring(e,t,r),stopMonitoring:e=>F.stopMonitoring(e),isMonitoring:e=>F.isMonitoring(e),destroy:()=>F.destroy()},experiences:{get:e=>T.get(e),enter:e=>T.enter(e),leave:e=>T.leave(e),getMe:()=>T.getMe(),findSequence:(e,t)=>T.findSequence(e,t),validateSequenceAccess:(e,t)=>T.validateSequenceAccess(e,t),selectSequence:e=>T.selectSequence(e),getCurrentDistributions:(e,t)=>T.getCurrentDistributions(e,t),enterDistribution:e=>{if(!e)throw new h("Distribution is required",v.VALIDATION_ERROR);return T.enterDistribution(e)},getActiveSession:()=>T.getActiveSession(),isInExperience:e=>T.isInExperience(e),getSelectedSequence:()=>T.getSelectedSequence(),createJourney:e=>Y(e),resumeJourneysFromMe:e=>Z(e),destroy:()=>T.destroy()},waitlists:{enter:(e,t)=>X.enter(e,g({waitlistId:e},t)),leave:(e,t)=>X.leave(e,g({waitlistId:e},t)),getStatus:e=>X.getStatus(e),destroy:()=>X.destroy()},reservations:J,timedReleases:{get:e=>P.get(e),enter:(e,t,r)=>P.enter(e,t,g({distributionId:e},r)),leave:(e,t)=>P.leave(e,g({distributionId:e},t)),complete:e=>P.complete(e),status:e=>P.status(e),startMonitoring:(e,t,r)=>P.startMonitoring(e,t,r),stopMonitoring:e=>P.stopMonitoring(e),isMonitoring:e=>P.isMonitoring(e)},appointments:{get:(e,t)=>K.get(e,g({distributionId:e},t)),getSlots:(e,t,r)=>K.getSlots(e,t,g({distributionId:e},r)),getMe:e=>K.getMe(e),book:(e,t,r,n)=>K.book(e,t,r,g({distributionId:e},n)),cancel:(e,t,r,n,i)=>K.cancel(e,t,r,n,g({distributionId:e},i)),reschedule:(e,t,r,n)=>K.reschedule(e,t,r,g({distributionId:e},n)),startMonitoring:(e,t,r,n)=>K.startMonitoring(e,t,r,n),stopMonitoring:e=>K.stopMonitoring(e),isMonitoring:e=>K.isMonitoring(e),bumpMonitorGeneration:e=>K.bumpMonitorGeneration(e),destroy:()=>K.destroy()},beacon:re,payment:$,use:ne,on:ie,destroy:oe};const se=!1!==j.sync&&"object"==typeof j.sync?j.sync:{};_=new p({enabled:!1!==j.sync&&!1!==se.enabled,channelName:se.channelName}),x.info("Inter-tab sync initialized",{tabId:_.getTabId(),enabled:_.isEnabled()});const ae={auth:W,challenges:ee,journeys:te,beacon:re,appointments:H.appointments,payment:$,use:ne,on:ie,destroy:oe};if(await R.rehydrate(),k.autoResume&&R.getSession())try{await Q()}catch(de){x.warn("Auto-resume on init failed; journeys can be resumed manually",{error:de instanceof Error?de.message:String(de)})}return S=D,x.info("SDK initialized successfully"),ae}export{j as init};
1
+ import{createBeaconLifecycleAdapter as e}from"../adapters/beacon-lifecycle.js";import{AppointmentManagementModule as t}from"../appointments/appointment.module.js";import{AuctionManagementModule as i}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 o}from"../challenges/challenge.module.js";import{Config as s}from"../config/index.js";import{DrawManagementModule as a}from"../draws/draw.module.js";import{ExperienceManagementModule as d}from"../experiences/experience.module.js";import{ExperienceJourney as u}from"../experiences/journey.js";import{PaymentMethodManagementModule as l}from"../payment/payment.module.js";import{QueueManagementModule as c}from"../queues/queue.module.js";import{resolveCapabilityOptions as g,resolveCapabilityAttribution as y}from"../state/capability-token-registry.js";import{getEventBus as m}from"../state/events.js";import{getSDKStore as p}from"../state/store.js";import{CrossTabCoordinator as f}from"../sync/cross-tab-coordinator.js";import{TimedReleaseManagementModule as b}from"../timed-releases/timed-release.module.js";import{version as w}from"../version.js";import{WaitlistManagementModule as I}from"../waitlists/waitlist.module.js";import{FanfareError as h,ErrorCodes as v}from"./errors.js";import{createHttpClient as M}from"./http.js";import{createLogger as S}from"./logger.js";let j=null;async function k(k){const A=new s(k),x=A.get(),D=S({enabled:x.debug||!1,level:x.debug?"debug":"warn",prefix:"Fanfare"});if(D.info("Initializing Fanfare SDK",{organizationId:k.organizationId,environment:x.environment}),j)throw new h("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.",v.ALREADY_INITIALIZED);const E=/* @__PURE__ */Symbol("fanfare-sdk"),C=M({baseUrl:A.apiUrl,credentials:x.credentials,headers:{"X-Organization-Id":k.organizationId,"X-Publishable-Key":k.publishableKey,"X-Fanfare-API-Version":"2025-01-15","X-Fanfare-SDK-Version":w,"X-Fanfare-Client-Type":"browser"},timeout:3e4,retryConfig:{maxRetries:3,delayTimer:1e3,retryOnNetworkError:!0}},x),q=M({baseUrl:A.beaconUrl,credentials:"omit",headers:{"X-Organization-Id":k.organizationId,"X-Publishable-Key":k.publishableKey,"X-Fanfare-API-Version":"2025-01-15","X-Fanfare-SDK-Version":w,"X-Fanfare-Client-Type":"browser"},timeout:3e4,retryConfig:{maxRetries:3,delayTimer:1e3,retryOnNetworkError:!0}},x),R=new r(C,q),z=new o(C),O=new c(C),F=new a(C),K=new i(C),X=new t(C),P=new I(C),T=new b(C),$=new d(C,O,F,K,P,T),J=new l(C),N=m(),V=p();N.on("auth:logout",()=>J.destroy());const B=/* @__PURE__ */new Map,L=/* @__PURE__ */new Map,U=new n(q,x.beacon||{},e=>{const t=L.get(e)?.snapshot$.get();return"routed"===t?.journeyStage?t.sequenceId:void 0}),G=/* @__PURE__ */new Map;let H,_;const Y=e=>{const t=L.get(e);if(t)return D.debug("Returning existing journey handle",{experienceId:e}),t;D.info("Creating new journey",{experienceId:e});const i=new u(e,H);B.set(e,i),_?.registerJourney(i);const r={view$:i.view$,events$:i.events$,latestEvent$:i.latestEvent$,snapshot$:i.state,ackEvent:e=>i.ackEvent(e),ackAllEvents:()=>i.ackAllEvents(),destroy:()=>{i.destroy(),_?.unregisterJourney(e),B.delete(e),L.delete(e)}};return L.set(e,r),r},Z=async e=>{const t=e??await $.getMe(),i=/* @__PURE__ */new Set,r=new Set(t.active.journeys.map(e=>e.experienceId));for(const n of r)Y(n);for(const n of r){const e=B.get(n);e&&await e.resumeFromMe(t)&&i.add(n)}return[...i]},Q=async e=>{const t=/* @__PURE__ */new Set;for(const i of Object.keys(V.activeJourneys))Y(i),t.add(i);if(!e&&!R.getSession())return[...t];for(const i of await Z(e))t.add(i);return[...t]},W={check:()=>R.check(),guest:()=>R.guest(),requestOtp:e=>R.requestOtp(e),verifyOtp:e=>R.verifyOtp(e),exchangeExternal:e=>R.exchangeExternal(e),login:e=>R.login(e),logout:()=>R.logout(),getSession:()=>R.getSession(),refresh:()=>R.refresh()},ee={initiate:e=>z.initiate(e),verify:e=>z.verify(e)},te={get:e=>Y(e),list:()=>Array.from(
2
+ /* @__PURE__ */new Set([...L.keys(),...B.keys(),...Object.keys(V.activeJourneys)])),resumeAll:e=>Q(e)},ie={bind:e=>U.bind(e),track:e=>U.track(e),trackBatch:e=>U.trackBatch(e),flush:()=>U.flush()},re=(e,t)=>{if(G.has(e.id))D.warn("CDP adapter already registered, skipping",{adapterId:e.id});else{try{e.init(N,t)}catch(i){return void D.error("CDP adapter failed to initialize",{adapterId:e.id,error:i})}G.set(e.id,e),D.info("CDP adapter registered",{adapterId:e.id})}},ne=(e,t)=>N.on(e,t),oe=async()=>{D.info("Destroying SDK instance");for(const t of G.values())try{t.destroy()}catch(e){D.error("CDP adapter failed to destroy",{adapterId:t.id,error:e})}G.clear();for(const t of Array.from(L.values()))try{t.destroy()}catch(e){D.error("Journey handle failed to destroy",{error:e})}B.clear(),L.clear(),"destroy"in R&&"function"==typeof R.destroy&&R.destroy(),O.destroy(),F.destroy(),K.destroy(),X.destroy(),P.destroy(),T.destroy(),$.destroy(),U.destroy(),J.destroy(),_?.close(),N.off(),j===E&&(j=null)};H={auth:W,challenges:ee,journeys:te,queues:{get:e=>O.get(e),enter:(e,t,i,r)=>O.enter(e,t,g({distributionId:e},i),r,y({distributionId:e})),leave:(e,t)=>O.leave(e,g({distributionId:e},t)),status:e=>O.status(e),startMonitoring:(e,t,i)=>O.startMonitoring(e,t,i),stopMonitoring:e=>O.stopMonitoring(e),isMonitoring:e=>O.isMonitoring(e)},draws:{get:e=>F.get(e),enter:(e,t,i,r)=>F.enter(e,t,g({distributionId:e},i),r,y({distributionId:e})),leave:(e,t)=>F.leave(e,g({distributionId:e},t)),status:e=>F.status(e),startMonitoring:(e,t,i)=>F.startMonitoring(e,t,i),stopMonitoring:e=>F.stopMonitoring(e),isMonitoring:e=>F.isMonitoring(e)},auctions:{get:e=>K.get(e),bid:(e,t,i)=>K.bid(e,t,g({distributionId:e},i),y({distributionId:e})),enter:(e,t,i)=>K.enter(e,t,g({distributionId:e},i),y({distributionId:e})),leave:(e,t)=>K.leave(e,g({distributionId:e},t)),status:e=>K.status(e),getBidHistory:e=>K.getBidHistory(e),enableAutoRebid:(e,t,i)=>K.enableAutoRebid(e,t,i),disableAutoRebid:e=>K.disableAutoRebid(e),getAutoRebidConfig:e=>K.getAutoRebidConfig(e),startMonitoring:(e,t,i)=>K.startMonitoring(e,t,i),stopMonitoring:e=>K.stopMonitoring(e),isMonitoring:e=>K.isMonitoring(e),destroy:()=>K.destroy()},experiences:{get:e=>$.get(e),enter:e=>$.enter(e),leave:e=>$.leave(e),getMe:()=>$.getMe(),confirmCheckout:(e,t,i)=>$.confirmCheckout(e,t,i),findSequence:(e,t)=>$.findSequence(e,t),validateSequenceAccess:(e,t)=>$.validateSequenceAccess(e,t),selectSequence:e=>$.selectSequence(e),getCurrentDistributions:(e,t)=>$.getCurrentDistributions(e,t),enterDistribution:e=>{if(!e)throw new h("Distribution is required",v.VALIDATION_ERROR);return $.enterDistribution(e)},getActiveSession:()=>$.getActiveSession(),isInExperience:e=>$.isInExperience(e),getSelectedSequence:()=>$.getSelectedSequence(),createJourney:e=>Y(e),resumeJourneysFromMe:e=>Z(e),destroy:()=>$.destroy()},waitlists:{enter:(e,t)=>P.enter(e,g({waitlistId:e},t),y({waitlistId:e})),leave:(e,t)=>P.leave(e,g({waitlistId:e},t),y({waitlistId:e})),getStatus:e=>P.getStatus(e),destroy:()=>P.destroy()},timedReleases:{get:e=>T.get(e),enter:(e,t,i)=>T.enter(e,t,g({distributionId:e},i),y({distributionId:e})),leave:(e,t)=>T.leave(e,g({distributionId:e},t)),complete:e=>T.complete(e),status:e=>T.status(e),startMonitoring:(e,t,i)=>T.startMonitoring(e,t,i),stopMonitoring:e=>T.stopMonitoring(e),isMonitoring:e=>T.isMonitoring(e)},appointments:{get:(e,t)=>X.get(e,g({distributionId:e},t)),getSlots:(e,t,i)=>X.getSlots(e,t,g({distributionId:e},i)),getMe:e=>X.getMe(e),book:(e,t,i,r)=>X.book(e,t,i,g({distributionId:e},r),y({distributionId:e})),cancel:(e,t,i,r,n)=>X.cancel(e,t,i,r,g({distributionId:e},n)),reschedule:(e,t,i,r)=>X.reschedule(e,t,i,g({distributionId:e},r)),startMonitoring:(e,t,i,r)=>X.startMonitoring(e,t,i,r),stopMonitoring:e=>X.stopMonitoring(e),isMonitoring:e=>X.isMonitoring(e),bumpMonitorGeneration:e=>X.bumpMonitorGeneration(e),destroy:()=>X.destroy()},beacon:ie,payment:J,use:re,on:ne,destroy:oe};const se=!1!==k.sync&&"object"==typeof k.sync?k.sync:{};_=new f({enabled:!1!==k.sync&&!1!==se.enabled,channelName:se.channelName}),D.info("Inter-tab sync initialized",{tabId:_.getTabId(),enabled:_.isEnabled()});!x.beacon?.disableLifecycleEvents&&re(e(e=>U.track(e)));const ae={auth:W,challenges:ee,journeys:te,beacon:ie,appointments:H.appointments,payment:J,use:re,on:ne,destroy:oe};if(await R.rehydrate(),x.autoResume&&R.getSession())try{await Q()}catch(de){D.warn("Auto-resume on init failed; journeys can be resumed manually",{error:de instanceof Error?de.message:String(de)})}return j=E,D.info("SDK initialized successfully"),ae}export{k as init};
@@ -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.OVERLOADED]:"INLINE",[E.SERVICE_UNAVAILABLE]:"INLINE",[E.TIMEOUT]:"INLINE",[E.NETWORK_ERROR]:"INLINE",[E.FEATURE_DISABLED]:"INLINE",[E.PAYMENT_REQUIRED]:"INLINE",[E.PAYMENT_FAILED]:"INLINE",[E.PAYMENT_AUTHENTICATION_FAILED]:"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.NEW_SLOT_FULL]:"STEP_RESET",[E.RESERVATION_EXPIRED]:"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_DEACTIVATED]:"PANEL",[E.DISTRIBUTION_FULL]:"PANEL",[E.DISTRIBUTION_ORDER_LIMIT]:"PANEL",[E.PROTOCOL_ERROR]:"PANEL",[E.NO_ACCESS]:"PANEL",[E.CHECKOUT_MISCONFIGURED]:"PANEL"},T=/* @__PURE__ */new Set([E.ENTRY_TOKEN_MISMATCH]);function N(E,N,R){const A=function(E,N){return"reroute"===N&&T.has(E)?"RESTART":I[E]??"PANEL"}(E,R);return{disposition:A,autoRetryOnce:false}}export{N 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.PAYMENT_REQUIRED]:"INLINE",[E.PAYMENT_FAILED]:"INLINE",[E.PAYMENT_AUTHENTICATION_FAILED]:"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.NEW_SLOT_FULL]:"STEP_RESET",[E.ADMISSION_EXPIRED]:"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_DEACTIVATED]:"PANEL",[E.DISTRIBUTION_FULL]:"PANEL",[E.DISTRIBUTION_ORDER_LIMIT]:"PANEL",[E.PROTOCOL_ERROR]:"PANEL",[E.NO_ACCESS]:"PANEL",[E.CHECKOUT_MISCONFIGURED]:"PANEL"},N=/* @__PURE__ */new Set([E.ENTRY_TOKEN_MISMATCH]);function T(E,T,R){const A=function(E,T){return"reroute"===T&&N.has(E)?"RESTART":I[E]??"PANEL"}(E,R);return{disposition:A,autoRetryOnce:false}}export{T as classify};
@@ -28,8 +28,6 @@ export declare const CheckoutErrorCodes: {
28
28
  readonly PAYMENT_REQUIRED: "PAYMENT_REQUIRED";
29
29
  readonly PAYMENT_FAILED: "PAYMENT_FAILED";
30
30
  readonly PAYMENT_AUTHENTICATION_FAILED: "PAYMENT_AUTHENTICATION_FAILED";
31
- readonly RESERVATION_EXPIRED: "RESERVATION_EXPIRED";
32
- readonly RESERVATION_NOT_FOUND: "RESERVATION_NOT_FOUND";
33
31
  readonly CHECKOUT_MISCONFIGURED: "CHECKOUT_MISCONFIGURED";
34
32
  };
35
33
  export type CheckoutErrorCode = (typeof CheckoutErrorCodes)[keyof typeof CheckoutErrorCodes];
@@ -42,8 +40,6 @@ export declare const ErrorCodes: {
42
40
  readonly PAYMENT_REQUIRED: "PAYMENT_REQUIRED";
43
41
  readonly PAYMENT_FAILED: "PAYMENT_FAILED";
44
42
  readonly PAYMENT_AUTHENTICATION_FAILED: "PAYMENT_AUTHENTICATION_FAILED";
45
- readonly RESERVATION_EXPIRED: "RESERVATION_EXPIRED";
46
- readonly RESERVATION_NOT_FOUND: "RESERVATION_NOT_FOUND";
47
43
  readonly CHECKOUT_MISCONFIGURED: "CHECKOUT_MISCONFIGURED";
48
44
  readonly NETWORK_ERROR: "NETWORK_ERROR";
49
45
  readonly TIMEOUT: "TIMEOUT";
@@ -76,6 +72,8 @@ export declare const ErrorCodes: {
76
72
  readonly ADMISSION_ORIGINAL_ENTRY_REQUIRED: "ADMISSION_ORIGINAL_ENTRY_REQUIRED";
77
73
  readonly ADMISSION_KEY_MISMATCH: "ADMISSION_KEY_MISMATCH";
78
74
  readonly ADMISSION_ACTIVE: "ADMISSION_ACTIVE";
75
+ readonly ADMISSION_EXPIRED: "ADMISSION_EXPIRED";
76
+ readonly ADMISSION_NOT_FOUND: "ADMISSION_NOT_FOUND";
79
77
  readonly DISTRIBUTION_NOT_OPEN: "DISTRIBUTION_NOT_OPEN";
80
78
  readonly DISTRIBUTION_CLOSED: "DISTRIBUTION_CLOSED";
81
79
  readonly DISTRIBUTION_DEACTIVATED: "DISTRIBUTION_DEACTIVATED";