@fanfare-io/fanfare-sdk-core 0.14.0 → 0.16.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.
- package/README.md +50 -0
- package/dist/appointments/appointment.driver.js +1 -1
- package/dist/appointments/appointment.module.js +1 -1
- package/dist/appointments/appointment.sequence.d.ts +5 -4
- package/dist/appointments/types.d.ts +3 -5
- package/dist/auctions/auction-status.js +1 -1
- package/dist/auctions/auction.driver.js +1 -1
- package/dist/auctions/auction.module.js +1 -1
- package/dist/auctions/auction.sequence.d.ts +89 -20
- package/dist/auctions/auction.sequence.js +1 -1
- package/dist/auctions/dutch-pricing.js +1 -0
- package/dist/auctions/public.d.ts +1 -1
- package/dist/auctions/types.d.ts +56 -29
- package/dist/beacon/types.d.ts +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +1 -1
- package/dist/core/client.js +2 -2
- package/dist/core/error-display.js +1 -1
- package/dist/core/error-disposition.d.ts +9 -0
- package/dist/core/error-disposition.js +1 -1
- package/dist/core/errors.d.ts +2 -3
- package/dist/core/errors.js +1 -1
- package/dist/draws/draw.driver.js +1 -1
- package/dist/draws/draw.module.js +1 -1
- package/dist/draws/draw.sequence.d.ts +85 -10
- package/dist/draws/types.d.ts +10 -1
- package/dist/draws/types.js +1 -1
- package/dist/experiences/distribution-monitor.types.d.ts +79 -18
- package/dist/experiences/experience.module.js +1 -1
- package/dist/experiences/journey-view.js +1 -1
- package/dist/experiences/journey.js +1 -1
- package/dist/experiences/journey.machine.d.ts +2 -0
- package/dist/experiences/journey.machine.js +1 -1
- package/dist/experiences/journey.types.d.ts +6 -5
- package/dist/experiences/public.d.ts +4 -2
- package/dist/experiences/types.d.ts +0 -4
- package/dist/internals.d.ts +1 -1
- package/dist/internals.js +1 -1
- package/dist/queues/queue.driver.js +1 -1
- package/dist/queues/queue.module.js +1 -1
- package/dist/queues/queue.sequence.d.ts +64 -3
- package/dist/queues/types.d.ts +1 -0
- package/dist/selections/public.d.ts +6 -0
- package/dist/selections/public.js +1 -0
- package/dist/selections/selection.module.d.ts +38 -0
- package/dist/selections/selection.module.js +1 -0
- package/dist/selections/types.d.ts +48 -0
- package/dist/ssr/ssr-sdk.js +1 -1
- package/dist/state/events.d.ts +0 -31
- package/dist/timed-releases/timed-release.driver.js +1 -1
- package/dist/timed-releases/timed-release.module.js +1 -1
- package/dist/timed-releases/timed-release.sequence.d.ts +43 -5
- package/dist/timed-releases/types.d.ts +6 -1
- package/dist/types/index.d.ts +3 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +12 -8
package/README.md
CHANGED
|
@@ -93,6 +93,56 @@ The framework adapters expose a tracker already bound to the surrounding experie
|
|
|
93
93
|
`useBeaconTracker` from `@fanfare-io/fanfare-sdk-react` and `createBeaconTracker` from
|
|
94
94
|
`@fanfare-io/fanfare-sdk-solid`.
|
|
95
95
|
|
|
96
|
+
## Browser origins and public keys
|
|
97
|
+
|
|
98
|
+
Publishable keys identify your organization and are intended to appear in storefront
|
|
99
|
+
code. They are not passwords. Consumer authentication and any required admission or
|
|
100
|
+
capability proof still authorize protected operations.
|
|
101
|
+
|
|
102
|
+
The SDK's explicit-token API works from any browser origin; a headless storefront
|
|
103
|
+
does not need a per-customer CORS allowlist entry. The organization's **Embedding**
|
|
104
|
+
settings control which sites can frame a hosted Fanfare page, not which sites can call
|
|
105
|
+
the API or render the SDK directly. Neither CORS nor an embedding allowlist replaces
|
|
106
|
+
server-side authorization. Never put secret API keys in browser code.
|
|
107
|
+
|
|
108
|
+
## Page observation
|
|
109
|
+
|
|
110
|
+
Use `journey.view$` for custom UI. The pure helpers `describeJourneyState(view)` and
|
|
111
|
+
`selectJourneyDisplay(view)` from `@fanfare-io/fanfare-sdk-core/experiences` describe
|
|
112
|
+
current state and select an available participating display atom, respectively.
|
|
113
|
+
They do not subscribe or start monitoring. React/Solid integrations should use their
|
|
114
|
+
existing journey hooks and `useSdkEvent` rather than use the DOM as a second state API.
|
|
115
|
+
|
|
116
|
+
For CSS and merchant scripts outside the SDK integration, install
|
|
117
|
+
`createDomBridge(journey, { target })` from that same public entry point. Use one
|
|
118
|
+
already-mounted, stable target per bridge; omitting it uses the document root.
|
|
119
|
+
The bridge returns `getState()` and an idempotent `dispose()`.
|
|
120
|
+
|
|
121
|
+
- Installation stamps current attributes without an initial event; subsequent
|
|
122
|
+
projection changes dispatch the bubbling, composed `fanfare:state-change` event.
|
|
123
|
+
- The attributes are `data-fanfare-experience-id`, `data-fanfare-stage`,
|
|
124
|
+
`data-fanfare-phase`, `data-fanfare-mechanism`, and `data-fanfare-expires-at` where
|
|
125
|
+
applicable. Expiry is Unix milliseconds. Ignore unknown attributes; absent ones
|
|
126
|
+
do not imply another state or SDK version.
|
|
127
|
+
- Queue position and terminal outcome type are payload-only; free-text outcome
|
|
128
|
+
reasons never cross the bridge. Read `getState()` when an immediate payload is needed.
|
|
129
|
+
- `@fanfare-io/fanfare-sdk-contracts/bridge` exports `FANFARE_STATE_CHANGE_EVENT`,
|
|
130
|
+
`BRIDGE_EVENT_VERSION`, `FanfareStateChangeDetail`, and `FanfareStateChangeEvent`
|
|
131
|
+
without importing the SDK. For a direct import, first run
|
|
132
|
+
`npm install @fanfare-io/fanfare-sdk-contracts`; a transitive dependency is not
|
|
133
|
+
an application import under strict package isolation. Check payload version `1` and use `detail.experienceId`
|
|
134
|
+
for attribution across shadow roots.
|
|
135
|
+
- Dispose before destroying the journey or SDK. This restores the bridge's attributes
|
|
136
|
+
and stops observation; it does not leave a distribution or unsubscribe a waitlist.
|
|
137
|
+
|
|
138
|
+
Once installed, surrounding CSS needs no SDK import:
|
|
139
|
+
|
|
140
|
+
```css
|
|
141
|
+
[data-fanfare-stage="routed"][data-fanfare-phase="participating"] .entry-prompt {
|
|
142
|
+
display: none;
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
96
146
|
## Documentation
|
|
97
147
|
|
|
98
148
|
- [Core SDK quickstart](https://docs.fanfare.io/sdk/core/quickstart)
|
|
@@ -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"
|
|
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",..."killed"===t.cancelledCause?{reason:"killed"}:{}}:void 0}(r);return p?s(a,r,p):function(t){return"completed"!==t.status&&"cancelled"!==t.status&&"not_booked"!==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",cancelledCause:"killed"===e.reason?"killed":t.cancelledCause}:"completed"===e.type?{...t,status:"completed"}:t: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:()=>this.cancel(),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(){await this.deps.runSerializedOperation(async()=>{const e=this.currentAppointmentSequence("participating"),i=e.consumer,n=this.deps.getDisplayState(),o=n&&"appointment"===n.type?n.booking:void 0;if(!o)throw t.validationError("No booking details available to cancel");const s=i.distributionId??i.id??e.distribution.id;await this.deps.appointments.cancel(s,o.slotId,o.locationId??""),this.deps.stopRuntimeMonitoring(!0),this.deps.applyMonitorUpdate({type:"ended",outcome:{type:"cancelled"}})})}async reschedule(t,e){await this.deps.runSerializedOperation(async()=>{const n=this.currentAppointmentSequence("participating"),o=n.consumer,s=o.distributionId??o.id??n.distribution.id,a=await this.deps.appointments.reschedule(s,t,e),r=this.deps.getCurrentSequence();r&&("participating"===r.phase&&"mechanism"in r&&"appointment"===r.mechanism&&r.distribution.id===n.distribution.id?(this.writeBookingDisplay(s,a),this.deps.commit(i({distribution:n.distribution,consumer:{...o,mechanism:"appointment",status:a.consumerStatus,slotId:a.slotId,locationId:a.locationId,cancelledCause:a.cancelledCause}})),this.deps.emitEvent("sequence_change","info","user","Rescheduled appointment",{appointmentId:s,newSlotId:t,newLocationId:e})):this.deps.emitEvent("info","info","system","Reschedule receipt superseded by a terminal state",{appointmentId:s,newSlotId:t}))})}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,startTime:e.startTime,endTime:e.endTime,confirmationCode:e.confirmationCode,consumerStatus:e.consumerStatus,cancelledCause:e.cancelledCause},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
|
|
1
|
+
import*as t from"valibot";import{isFanfareError as o,createError as e}from"../core/errors.js";import{getLogger as n}from"../core/logger.js";import{parseResponse as i}from"../core/parse-response.js";import{createPollScheduler as s}from"../core/poll-scheduler.js";import{DistributionMonitorRuntime as r}from"../experiences/distribution-monitor.runtime.js";import{getEventBus as a}from"../state/events.js";function l(t,o,e){const n=i(t,o,{endpoint:e},{throwOnFailure:!0});if(!n.ok)throw n.error;return n.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()}),c=t.object({slots:t.array(d)}),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())}),p=t.string(),h=t.picklist(["consumer","admin","killed"]),u=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,cancelledCause:t.nullish(h),createdAt:t.string()}),g=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()),startTime:t.nullish(t.string()),endTime:t.nullish(t.string()),confirmationCode:t.nullish(t.string()),status:p,cancelledCause:t.nullish(h)}))}),I=t.object({booking:u,bookingId:t.string(),previousSlotId:t.string(),newSlotId:t.string(),confirmationCode:t.string(),status:t.string(),cancelledCause:t.nullish(h)});function b(t){const{appointmentId:o,status:e}=t;let i;return"booked"===e||"completed"===e||"cancelled"===e?i=e:(n().warn("Unknown appointment consumerStatus; treating as booked",{appointmentId:o,status:e}),i="booked"),{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:i,cancelledCause:t.cancelledCause??void 0}}function k(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 w{constructor(t,o){this.logger=n(),this.monitorRuntime=new r,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),e=l(m,o,`/appointments/${t}`);return{id:e.id,type:"appointment",openAt:e.openAt??void 0,closeAt:e.closeAt??void 0,slotDurationMinutes:e.slotDurationMinutes??void 0,bookingWindowOpenAt:e.openAt??void 0,bookingWindowCloseAt:e.closeAt??void 0,timezone:e.timeZone??void 0,locationId:e.locationId??void 0,totalSlots:e.totalSlots??void 0,availableSlots:e.availableSlots??void 0}}catch(i){if(this.logger.error("Failed to get appointment",{appointmentId:t,error:i}),o(i))throw i;throw e.networkError("Failed to get appointment",{error:i})}}async getSlots(t,n,i){try{const o=new URLSearchParams({startDate:n.startDate,endDate:n.endDate}),e=await this.http.get(`/appointments/${t}/slots?${o.toString()}`,i);return l(c,e,`/appointments/${t}/slots`).slots.map(k)}catch(s){if(this.logger.error("Failed to get appointment slots",{appointmentId:t,error:s}),o(s))throw s;throw e.networkError("Failed to get appointment slots",{error:s})}}async getMe(t){try{const o=await this.http.get(`/appointments/${t}/me`),e=l(g,o,`/appointments/${t}/me`);return e.booking?function(t){const{appointmentId:o,status:e}=t;let i;return"booked"===e||"completed"===e||"cancelled"===e?i=e:(n().warn("Unknown appointment consumerStatus; treating as booked",{appointmentId:o,status:e}),i="booked"),{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??void 0,consumerStatus:i,cancelledCause:t.cancelledCause??void 0}}(e.booking):null}catch(i){if(this.logger.error("Failed to get current booking",{appointmentId:t,error:i}),o(i))throw i;throw e.networkError("Failed to get current booking",{error:i})}}async book(t,n,i,s,r){try{const o=await this.http.post(`/appointments/${t}/book`,{slotId:n,locationId:i??null},s),e=b(l(u,o,`/appointments/${t}/book`));return a().emit("experience:distribution-entered",{distributionType:"appointment",distributionId:t,...r}),e}catch(d){if(this.logger.error("Failed to book appointment",{appointmentId:t,slotId:n,locationId:i,error:d}),o(d))throw d;throw e.networkError("Failed to book appointment",{error:d})}}async cancel(t,n,i,s){try{await this.http.post(`/appointments/${t}/cancel`,{slotId:n,locationId:i??null},s)}catch(r){if(this.logger.error("Failed to cancel appointment",{appointmentId:t,slotId:n,locationId:i,error:r}),o(r))throw r;throw e.networkError("Failed to cancel appointment",{error:r})}}async reschedule(t,n,i,s){try{const o=await this.http.post(`/appointments/${t}/reschedule`,{newSlotId:n,newLocationId:i??null},s),e=b(l(I,o,`/appointments/${t}/reschedule`).booking);if(this.isMonitoring(t)){const o=this.monitorRuntime.getDisplayAtom(t);o&&o.set({type:"appointment",booking:e,msUntilStart:Math.max(0,Date.parse(e.startTime)-Date.now())}),this.startMonitor(t,o?{displayAtom:o}:void 0,void 0,this.pollSchedulers.get(t)?.onDegradedChange,e)}return e}catch(r){if(this.logger.error("Failed to reschedule appointment",{appointmentId:t,newSlotId:n,newLocationId:i,error:r}),o(r))throw r;throw e.networkError("Failed to reschedule appointment",{error:r})}}startMonitoring(t,o,e,n){this.startMonitor(t,o,e,n)}startMonitor(t,o,e,n,i){if(this.destroyed)return;this.monitorRuntime.start(t,o,e),this.stopPolling(t);const r=this.monitorRuntime.getDisplayAtom(t)?.get();this.observedBooking.set(t,"appointment"===r?.type&&void 0!==r.booking);const a=(this.monitorGenerations.get(t)??0)+1;this.monitorGenerations.set(t,a);const l=s({baseMs:this.pollingIntervalMs,suppressBackoff:()=>!1,onDegradedChange:n,shouldContinue:()=>!this.destroyed&&this.monitorGenerations.get(t)===a&&this.pollSchedulers.has(t),run:async()=>{if(!this.destroyed&&this.monitorGenerations.get(t)===a)try{const o=i??await this.getMe(t);if(i=void 0,this.destroyed||this.monitorGenerations.get(t)!==a)return;const e=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||"completed"===o.consumerStatus)return this.monitorRuntime.notify(t,{type:"ended",outcome:{type:o.consumerStatus,..."cancelled"===o.consumerStatus&&"killed"===o.cancelledCause?{reason:"killed"}:{}}}),void this.stopMonitoring(t);const n=Math.max(0,Date.parse(o.startTime)-Date.now());if(!e)return;e.set({type:"appointment",booking:{slotId:o.slotId,locationId:o.locationId,locationName:o.locationName,startTime:o.startTime,endTime:o.endTime,confirmationCode:o.confirmationCode,consumerStatus:o.consumerStatus,cancelledCause:o.cancelledCause},msUntilStart:n})}catch(o){throw this.logger.error("Appointment polling tick failed",{id:t,error:o}),o}}});this.pollSchedulers.set(t,{scheduler:l,onDegradedChange:n}),l.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.scheduler.stop(),this.pollSchedulers.delete(t))}}export{w as AppointmentManagementModule};
|
|
@@ -16,6 +16,8 @@ export interface AppointmentSequenceConsumerState {
|
|
|
16
16
|
id?: string;
|
|
17
17
|
/** Distribution id this appointment booking belongs to. */
|
|
18
18
|
distributionId?: string;
|
|
19
|
+
/** Who or what ended the booking. */
|
|
20
|
+
cancelledCause?: "consumer" | "admin" | "killed";
|
|
19
21
|
/** Booked slot id from the appointment service. */
|
|
20
22
|
slotId?: string;
|
|
21
23
|
/** Booked location id from the appointment service. */
|
|
@@ -156,16 +158,15 @@ export interface AppointmentParticipatingView {
|
|
|
156
158
|
mechanism: "appointment";
|
|
157
159
|
/** Consumer appointment booking facts. */
|
|
158
160
|
consumer: AppointmentSequenceConsumerState;
|
|
159
|
-
/** Live appointment display facts, including slot, location, and
|
|
161
|
+
/** Live appointment display facts, including slot, location, and booking status. */
|
|
160
162
|
display$: ReadableAtom<AppointmentDisplayState>;
|
|
161
163
|
/**
|
|
162
164
|
* Cancel the booked appointment.
|
|
163
165
|
*
|
|
164
|
-
* Legal only while this view is current. The
|
|
165
|
-
* the server. The promise rejects if the booking cannot be cancelled or the
|
|
166
|
+
* Legal only while this view is current. The promise rejects if the booking cannot be cancelled or the
|
|
166
167
|
* server rejects the cancellation.
|
|
167
168
|
*/
|
|
168
|
-
cancel(
|
|
169
|
+
cancel(): Promise<void>;
|
|
169
170
|
/**
|
|
170
171
|
* Move the booking to another slot and optional location.
|
|
171
172
|
*
|
|
@@ -15,21 +15,19 @@ export interface AppointmentBooking {
|
|
|
15
15
|
bookingId: string;
|
|
16
16
|
appointmentId: string;
|
|
17
17
|
slotId: string;
|
|
18
|
+
cancelledCause?: "consumer" | "admin" | "killed";
|
|
18
19
|
locationId?: string;
|
|
19
20
|
locationName?: string;
|
|
20
|
-
/** Join URL for online appointments. Absent for in-person bookings. */
|
|
21
|
-
joinUrl?: string;
|
|
22
21
|
startTime: string;
|
|
23
22
|
endTime: string;
|
|
24
23
|
confirmationCode?: string;
|
|
25
|
-
consumerStatus: "booked" | "
|
|
24
|
+
consumerStatus: "booked" | "completed" | "cancelled";
|
|
26
25
|
}
|
|
27
26
|
export interface AppointmentModule {
|
|
28
27
|
get(appointmentId: string, options?: RequestOptions): Promise<AppointmentDetails>;
|
|
29
28
|
getSlots(appointmentId: string, range: {
|
|
30
29
|
startDate: string;
|
|
31
30
|
endDate: string;
|
|
32
|
-
timezone?: string;
|
|
33
31
|
}, options?: RequestOptions): Promise<AppointmentSlot[]>;
|
|
34
32
|
/**
|
|
35
33
|
* Own-booking-scoped read. Ungated server-side, so it carries no capability
|
|
@@ -37,7 +35,7 @@ export interface AppointmentModule {
|
|
|
37
35
|
*/
|
|
38
36
|
getMe(appointmentId: string): Promise<AppointmentBooking | null>;
|
|
39
37
|
book(appointmentId: string, slotId: string, locationId?: string, options?: RequestOptions): Promise<AppointmentBooking>;
|
|
40
|
-
cancel(appointmentId: string, slotId: string, locationId?: string,
|
|
38
|
+
cancel(appointmentId: string, slotId: string, locationId?: string, options?: RequestOptions): Promise<void>;
|
|
41
39
|
reschedule(appointmentId: string, newSlotId: string, newLocationId?: string, options?: RequestOptions): Promise<AppointmentBooking>;
|
|
42
40
|
startMonitoring(id: string, context?: Record<string, unknown>, onUpdate?: (update: MonitorUpdate) => void, onDegradedChange?: (degraded: boolean) => void): void;
|
|
43
41
|
stopMonitoring(id: string): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
function n(n){switch(n){case"NOT_BID":return"not_bid";case"
|
|
1
|
+
function n(n){switch(n){case"NOT_BID":return"not_bid";case"WINNING":return"winning";case"OUTBID":return"outbid";case"WON":return"won";case"LOST":return"lost"}}function t(n){return"won"===n||"lost"===n}function r(n){return"won"===n.status&&!0===n.degraded&&!n.admissionGrant}export{r as isGrantPendingWin,t as isTerminalAuctionStatus,n as toAuctionConsumerStatus};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{AuctionDistributionResults as t}from"@fanfare-io/fanfare-sdk-contracts";import{createError as i}from"../core/errors.js";import{
|
|
1
|
+
import{AuctionDistributionResults as t}from"@fanfare-io/fanfare-sdk-contracts";import{createError as i}from"../core/errors.js";import{buildGrantPaymentAffordance as e}from"../experiences/sequence-driver.js";import{isGrantPendingWin as n}from"./auction-status.js";import{isUnresolvedAuctionConsumer as s}from"./auction.sequence.js";function o(t){return!t||("lost"===t.type||"closed"===t.type)&&!t.reason}function a(t){return new u(t)}function r(e){const{distribution:n,consumer:a,grant:r,settlement:c,selection:u,outcome:d}=e;if(!n)return{phase:"unavailable"};const m=function(t){if("auction"!==t.type)throw i.validationError("Auction driver received a non-auction distribution");return t}(n),h=a&&"auction"===a.mechanism?function(t){const{mechanism:i,...e}=t;return e}(a):l(n);if(r)return"completed"===d?.type?p(m,h,d):r.expiresAt&&Date.now()>new Date(r.expiresAt).getTime()?"open"===m.lifecycle?{phase:"enterable",mechanism:"auction",distribution:m,consumer:l(m)}:p(m,h,{type:"expired"}):c&&u?{phase:"granted",mechanism:"auction",distribution:m,consumer:h,grant:r,settlement:c,selection:u}:{phase:"unavailable"};if("closed"===m.lifecycle){if("not_bid"===h.status&&m.result===t.FLOOR_UNSOLD&&o(d))return p(m,h,{type:"lost",reason:t.FLOOR_UNSOLD});if("auction"===a?.mechanism&&!d)return p(m,h,{type:"lost"})}const b=d??function(t){return"lost"===t.status?{type:"lost"}:void 0}(h);return b?p(m,h,b):"settling"===m.lifecycle?{phase:"settling",mechanism:"auction",distribution:m,consumer:h}:s(h)?{phase:"participating",mechanism:"auction",distribution:m,consumer:h}:"scheduled"===m.lifecycle?{phase:"scheduled",mechanism:"auction",distribution:m}:"open"===m.lifecycle?{phase:"enterable",mechanism:"auction",distribution:m,consumer:h}:p(m,h,{type:"closed"})}function c(t,i,e){return t&&"auction"===t.mechanism&&i?"won"===i.type?{...t,status:"won"}:!e?.finalize&&"not_bid"===t.status&&o(i)?t:{...t,status:"lost"}:t}class u{constructor(t){this.deps=t,this.mechanism="auction"}project(t){return r(t)}buildView(t,n){if("unavailable"===t.phase)return{phase:"unavailable",reason:t.reason};const s=function(t){if(!("mechanism"in t)||"auction"!==t.mechanism)throw i.validationError("Auction driver received a non-auction sequence");return t}(t);switch(s.phase){case"scheduled":return{phase:"scheduled",mechanism:"auction",distribution:s.distribution,startsAt:s.distribution.startsAt??s.distribution.opensAt};case"enterable":return{phase:"enterable",mechanism:"auction",distribution:s.distribution,consumer:s.consumer,display$:n.displayAtom,loadSelectionOptions:n.loadSelectionOptions,bid:t=>this.bid(t)};case"participating":return{phase:"participating",mechanism:"auction",consumer:s.consumer,display$:n.displayAtom,loadSelectionOptions:n.loadSelectionOptions,bid:t=>this.bid(t)};case"settling":return{phase:"settling",mechanism:"auction",distribution:s.distribution,consumer:s.consumer};case"granted":{const t={phase:"granted",mechanism:"auction",distribution:s.distribution,consumer:s.consumer,grant:s.grant},{settlement:i}=s,o=s.selection;if("awaiting_assignment"===o.status)return{...t,settlement:i,selection:o};if("required"===o.status)return{...t,settlement:i,selection:o,select:n.select,loadSelectionOptions:n.loadSelectionOptions};if("internal"===i.processor&&"at_end"===i.mode){const a=e(i,s.pendingAction,n);return a?{...t,settlement:i,selection:o,loadSelectionOptions:n.loadSelectionOptions,select:n.select,claim:()=>n.claim(),payment:a}:{...t,settlement:i,selection:o,loadSelectionOptions:n.loadSelectionOptions,select:n.select,claim:()=>n.claim()}}return"external"===i.processor?{...t,settlement:i,selection:o,loadSelectionOptions:n.loadSelectionOptions,select:n.select,claim:()=>n.claim()}:{phase:"unavailable",reason:"checkout_misconfigured"}}case"ended":return{phase:"ended",mechanism:"auction",consumer:s.consumer,outcome:s.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 bid(t){await this.deps.runSerializedOperation(async()=>{const i=this.currentAuctionSequenceForBid(),e=i.consumer.distributionId??i.consumer.id??i.distribution.id,s=i.consumer.selection,o="auction"===i.distribution.details?.type?i.distribution.details:void 0;d(t,o?.auctionType);const a=await this.deps.auctions.bid(e,t),c=this.deps.getCurrentSequence(),u=c&&("enterable"===c.phase||"participating"===c.phase||"settling"===c.phase)&&"mechanism"in c&&"auction"===c.mechanism&&c.distribution.id===e;if("won"===a.status||"lost"===a.status)return void(u&&("won"===a.status&&a.admissionGrant?this.deps.applyMonitorUpdate({type:"granted",token:a.admissionGrant}):"lost"===a.status?this.deps.applyMonitorUpdate({type:"ended",outcome:{type:"lost"}}):n(a)||this.deps.applyMonitorUpdate({type:"resync"})));if(!u)return;const l=function(t){switch(t){case"not_bid":case"winning":case"outbid":return t;case"won":case"lost":return}}(a.status);void 0!==l&&this.deps.commit(r({distribution:i.distribution,consumer:{..."participating"===i.phase?i.consumer:{},mechanism:"auction",status:l,id:e,distributionId:e,...s?{selection:s}:{},currentBid:a.amount,highestBid:a.highestBid}}))})}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 d(t,e){if("dutch"!==e){if(void 0===t.amount)throw i.validationError("A bid amount is required for an English auction")}else if(void 0!==t.amount)throw i.validationError("A Dutch auction clinches at the current price; omit the bid amount")}function l(t){return{status:"not_bid",distributionId:t.id}}function p(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{d as assertBidInputMatchesMechanism,a as createAuctionDriver,o as isUpgradeableGenericLoss,r as projectAuctionSequence,c 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,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
|
+
import{AuctionDetailsResponseSchema as t,AuctionBidderStateResponseSchema as i}from"@fanfare-io/fanfare-sdk-contracts/auction";import*as e from"valibot";import{createError as s}from"../core/errors.js";import{getLogger as n}from"../core/logger.js";import{isMoneyString as a,isMoneyGreaterThan as o,normalizeMoney as r}from"../core/money.js";import{parseResponse as d}from"../core/parse-response.js";import{createPollScheduler as c}from"../core/poll-scheduler.js";import{DistributionMonitorRuntime as u}from"../experiences/distribution-monitor.runtime.js";import{getEventBus as l}from"../state/events.js";import{getSDKStore as h}from"../state/store.js";import{isGrantPendingWin as p,toAuctionConsumerStatus as g,isTerminalAuctionStatus as m}from"./auction-status.js";import{assertBidInputMatchesMechanism as y}from"./auction.driver.js";import{advanceDutchPricing as S}from"./dutch-pricing.js";const f=/* @__PURE__ */Symbol("auction-participation-missing");function v(t){return"won"===t.status&&!t.admissionGrant&&!t.outcome}function b(t){return void 0!==t&&m(t.status)&&!v(t)}function A(t){return"english"===t.auctionType?r(t.highestBid):r(t.currentPrice)}const w=e.object({productId:e.string(),variantId:e.optional(e.string())});class D{constructor(t){this.logger=n(),this.events=l(),this.trackedParticipations=/* @__PURE__ */new Map,this.inFlightRequests=/* @__PURE__ */new Map,this.pollSchedulers=/* @__PURE__ */new Map,this.pollBaseMs=/* @__PURE__ */new Map,this.degradedHandlers=/* @__PURE__ */new Map,this.lastStatusCache=/* @__PURE__ */new Map,this.hasSuccessfulReadThisSession=/* @__PURE__ */new Map,this.lastSettleAtMs=/* @__PURE__ */new Map,this.visibilityHandlers=/* @__PURE__ */new Map,this.dutchStepTimers=/* @__PURE__ */new Map,this.monitorRuntime=new u,this.auctionDetailsCache=/* @__PURE__ */new Map,this.watchDetails=/* @__PURE__ */new Map,this.monitorGenerations=/* @__PURE__ */new Map,this.operationGenerations=/* @__PURE__ */new Map,this.AUCTION_DETAILS_CACHE_TTL=3e4,this.WATCHING_POLL_MS=15e3,this.PARTICIPATING_POLL_MS=5e3,this.NO_BID_END_GRACE_MS=5e3,this.destroyed=!1,this.lifecycleGeneration=0,this.http=t}get store(){return h()}async getAuctionDetailsCached(i,e){const s=this.auctionDetailsCache.get(i),n=void 0!==s&&Date.now()-s.fetchedAt<this.AUCTION_DETAILS_CACHE_TTL,a=void 0!==s&&"error"in s;if(s&&n&&(!e?.bypassCache||a)){if(void 0!==s.details)return s.details;if("error"in s)throw s.error}try{const e=await this.http.get(`/auctions/${i}`),s=d(t,e,{endpoint:`/auctions/${i}`},{throwOnFailure:!0});if(!s.ok)throw s.error;const n=s.value;return this.auctionDetailsCache.set(i,{details:n,fetchedAt:Date.now()}),n}catch(o){if(this.auctionDetailsCache.set(i,{...void 0!==s?.details?{details:s.details}:{},fetchedAt:Date.now(),error:o}),void 0!==s?.details)return s.details;throw o}}getTrackedParticipation(t){return this.trackedParticipations.get(t)}buildPricing(t,i){return"english"===t.auctionType?{model:"english",...null===t.highestBid?{}:{highestBid:t.highestBid},minNextBid:t.minNextBid,...i?.minBidIncrement?{bidIncrement:i.minBidIncrement}:{},bidCount:t.bidCount,effectiveSettleAt:t.effectiveSettleAt,autoExtended:t.autoExtended,reserveMet:t.reserveMet,ended:t.ended}:{model:"dutch",currentPrice:t.currentPrice,remainingQuantity:t.remainingQuantity,totalQuantity:t.totalQuantity,...null===t.nextDropAt?{}:{nextDropAt:t.nextDropAt},...null===t.floorPrice?{}:{floorPrice:t.floorPrice},settleAt:t.settleAt,...i?.startPrice?{startPrice:i.startPrice}:{},...i?.priceDropAmount?{priceDropAmount:i.priceDropAmount}:{},..."number"==typeof i?.priceDropIntervalSeconds?{priceDropIntervalSeconds:i.priceDropIntervalSeconds}:{},ended:t.ended}}buildDisplayState(t,i,e){const s=("english"===i.auctionType?i.closeAt:null)??e?.closeAt??void 0;return{status:t,...e?.currencyCode?{currencyCode:e.currencyCode}:{},...s?{closeAt:s}:{},pricing:this.buildPricing(i,e)}}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.displayState&&this.armDutchStep(t)}armDutchStep(t){this.clearDutchStep(t);const i=this.monitorRuntime.getDisplayAtom(t),e=i?.get();if(!i||"auction"!==e?.type)return;const s=e.pricing;if("dutch"!==s?.model||s.ended||!s.nextDropAt||void 0===s.priceDropAmount||void 0===s.priceDropIntervalSeconds)return;const n=Date.parse(s.nextDropAt);if(Number.isNaN(n))return;const a=Math.max(0,n-Date.now());if(a>2147483647)return;const o=setTimeout(()=>{this.dutchStepTimers.delete(t);const i=this.monitorRuntime.getDisplayAtom(t),e=i?.get();if(!i||"auction"!==e?.type||"dutch"!==e.pricing?.model)return;const s=S(e.pricing,Date.now());s!==e.pricing&&(i.set({...e,pricing:s}),this.armDutchStep(t))},a+1);this.dutchStepTimers.set(t,o)}clearDutchStep(t){const i=this.dutchStepTimers.get(t);void 0!==i&&(clearTimeout(i),this.dutchStepTimers.delete(t))}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,r=t.highestBid||"0";if(a(s)&&a(n)&&a(r))return o(n,r)&&o(n,s)&&"outbid"===i.status}return"outbid"!==t.status&&"outbid"===i.status}buildBidTransition(t,i,e,s,n){const a=i.amount,o=n.isFirstBid?[{type:"auction:entered",payload:{auctionId:t}},{type:"experience:distribution-entered",payload:{distributionType:"auction",distributionId:t,...n.attribution}}]:[],r={type:"auction:bid-placed",payload:{auctionId:t,amount:a,status:i.status}};if("won"===i.status&&!p(i)||"lost"===i.status){const s=this.buildTerminalTransition(t,i.status,i.live,e,{currentBid:a,admissionGrant:i.admissionGrant});return{...s,events:[...o,r,...s.events]}}return{trackedParticipation:{auctionId:t,enteredAt:s?.enteredAt??/* @__PURE__ */(new Date).toISOString(),status:i.status,currentBid:a,highestBid:i.highestBid,bidCount:i.bidCount,lastBidAt:/* @__PURE__ */(new Date).toISOString(),metadata:s?.metadata,fingerprint:s?.fingerprint},displayState:this.buildDisplayState(i.status,i.live,e),events:[...o,r,..."winning"===i.status?[{type:"auction:winning",payload:{auctionId:t,amount:a,highestBid:i.highestBid}}]:[],..."outbid"===i.status?[{type:"auction:outbid",payload:{auctionId:t,yourBid:a,highestBid:i.highestBid}}]:[]],monitorUpdate:null,stopWatching:!1}}buildTerminalTransition(t,i,e,s,n){return"won"===i?{trackedParticipation:null,displayState:this.buildDisplayState(i,e,s),events:[{type:"auction:won",payload:{auctionId:t,winningBid:n.currentBid??"",admissionGrant:n.admissionGrant}}],monitorUpdate:n.admissionGrant?{type:"granted",token:n.admissionGrant}:{type:"resync"},stopWatching:!0}:{trackedParticipation:null,displayState:this.buildDisplayState(i,e,s),events:[{type:"auction:lost",payload:{auctionId:t,highestBid:A(e)}}],monitorUpdate:{type:"ended",outcome:{type:"lost"}},stopWatching:!0}}buildStatusTransition(t,i,e,s,{previousParticipation:n,previousStatus:a}){const o=i.currentBid??n?.currentBid,r=i.bidCount??n?.bidCount;if(b(a))return{displayState:null,events:[],monitorUpdate:null,stopWatching:!1};if("won"===i.status&&!v(i)||"lost"===i.status)return this.buildTerminalTransition(t,i.status,e,s,{currentBid:o,admissionGrant:i.admissionGrant});const d=("english"===e.auctionType?e.closeAt:e.settleAt)??s?.closeAt,c=d?new Date(d).getTime():void 0,u=void 0!==c&&Date.now()>=c+this.NO_BID_END_GRACE_MS;if("not_bid"===i.status&&(e.ended||u))return{trackedParticipation:null,displayState:this.buildDisplayState("ended",e,s),events:[],monitorUpdate:{type:"ended",outcome:{type:"closed"}},stopWatching:!0};const l={auctionId:t,enteredAt:n?.enteredAt??/* @__PURE__ */(new Date).toISOString(),status:i.status,currentBid:o,highestBid:i.highestBid,bidCount:r,metadata:n?.metadata,fingerprint:n?.fingerprint},h=[];return a&&(this.checkOutbidConditions(a,i,n)&&h.push({type:"auction:outbid",payload:{auctionId:t,yourBid:a.currentBid||n?.currentBid||"",highestBid:i.highestBid}}),"winning"!==a.status&&"winning"===i.status&&h.push({type:"auction:winning",payload:{auctionId:t,amount:o||"",highestBid:i.highestBid}}),a.highestBid!==i.highestBid&&"winning"!==i.status&&h.push({type:"auction:bid-updated",payload:{auctionId:t,highestBid:i.highestBid,bidCount:i.bidCount||0}})),{trackedParticipation:l,displayState:this.buildDisplayState(i.status,e,s),events:h,monitorUpdate:null,stopWatching:!1}}buildMissingTransition(){return{trackedParticipation:null,displayState:{status:"lost"},events:[],monitorUpdate:{type:"ended",outcome:{type:"closed",reason:"not_participating"}},stopWatching:!0}}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(t,n,o,c){try{const u=this.lifecycleGeneration;if(void 0!==n.amount&&!a(n.amount))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");const l=this.bumpOperationGeneration(t),h=this.pollSchedulers.has(t),m=this.getTrackedParticipation(t),S=!(m&&"not_bid"!==m.status||b(this.lastStatusCache.get(t)));this.logger.info("Placing bid",{auctionId:t,amount:n.amount});const f=`/auctions/${t}/bid`,v=await this.readDetailsForDisplay(t);v&&y(n,v.auctionType);const D=await this.http.post(f,{...void 0===n.amount?{}:{amount:n.amount},...void 0===n.selection?{}:{selection:n.selection}},o),B=d(i,D,{endpoint:f},{throwOnFailure:!0});if(!B.ok)throw B.error;const C=B.value;C.degraded&&this.logger.warn("Bid committed; the credential mint did not complete on this request",{auctionId:t});const I=g(C.status),T=r(C.winningBidAmount??C.lastBidAmount??n.amount,A(C.live)),M="WON"===C.status&&void 0!==C.selection?e.safeParse(w,C.selection):void 0,P=M?.success?M.output:void 0,G={...P?{selection:P}:{},status:I,amount:T,highestBid:A(C.live),bidCount:C.bidCount??(m?.bidCount||0)+1,admissionGrant:C.admissionGrant,...C.degraded?{degraded:!0}:{},live:C.live};if(!this.canApplyAsyncResult(t,u,void 0,l))return G;if(b(this.lastStatusCache.get(t))||h&&!this.pollSchedulers.has(t))return G;if(this.applyTransition(t,this.buildBidTransition(t,G,v,m,{isFirstBid:S,attribution:c})),this.syncPollCadence(t),p(G))return G;return b(this.lastStatusCache.get(t))||this.lastStatusCache.set(t,{auctionId:t,status:I,currentBid:T,highestBid:G.highestBid,timeRemaining:0,bidCount:G.bidCount,admissionGrant:C.admissionGrant,live:C.live}),G}catch(u){throw this.logger.error("Failed to place bid",{auctionId:t,amount:n.amount,error:u}),this.events.emit("auction:error",{auctionId:t,error:u}),u}}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(t,e,s,n){try{const a=`/auctions/${t}/status`,o=await this.http.get(a).catch(t=>{if(function(t){return"object"==typeof t&&null!==t&&"status"in t&&404===t.status}(t))return f;throw t});if(o===f)return this.applyMissingStatus(t,e,s,n);const c=d(i,o,{endpoint:a},{throwOnFailure:!0});if(!c.ok)throw c.error;const u=c.value,l=await this.readDetailsForDisplay(t),h=u.live,p=new Date(function(t){return"english"===t.auctionType?t.effectiveSettleAt:t.settleAt}(h)).getTime(),m=A(h),y=this.getTrackedParticipation(t),S=u.lastBidAmount??u.winningBidAmount,v={auctionId:t,status:g(u.status),currentBid:void 0!==S?r(S):y?.currentBid,highestBid:m,timeRemaining:Number.isFinite(p)?Math.max(0,p-Date.now()):0,bidCount:u.bidCount,admissionGrant:u.admissionGrant,expiresAt:u.expiresAt,degraded:u.degraded,outcome:u.outcome,selection:u.selection,live:h};if(!this.canApplyAsyncResult(t,e,s,n))return v;const w=this.lastStatusCache.get(t),D=this.buildStatusTransition(t,v,h,l,{previousParticipation:y,previousStatus:w});return this.applyTransition(t,D),this.hasSuccessfulReadThisSession.set(t,!0),Number.isFinite(p)?this.lastSettleAtMs.set(t,p):this.lastSettleAtMs.delete(t),b(w)?this.lastStatusCache.set(t,w):b(v)?this.lastStatusCache.set(t,v):D.stopWatching?this.lastStatusCache.delete(t):this.lastStatusCache.set(t,v),this.syncPollCadence(t),this.events.emit("auction:status-updated",{auctionId:t,status:v}),v}catch(a){if(!this.canApplyAsyncResult(t,e,s,n))return this.lastStatusCache.get(t)??{auctionId:t,status:"not_bid",highestBid:"0.00",timeRemaining:0};throw this.logger.error("Failed to get auction status",{auctionId:t,error:a}),this.events.emit("auction:error",{auctionId:t,error:a}),a}}async readDetailsForDisplay(t){if(this.pollSchedulers.has(t)){const i=this.watchDetails.get(t);if(i)return i}try{const i=this.auctionDetailsCache.get(t),e=Date.now(),s=void 0!==i&&!("error"in i)&&e-i.fetchedAt<this.AUCTION_DETAILS_CACHE_TTL&&(null==i.details.openAt||e>=new Date(i.details.openAt).getTime()),n=await this.getAuctionDetailsCached(t,{bypassCache:this.pollSchedulers.has(t)&&s}),a=this.auctionDetailsCache.get(t);return this.pollSchedulers.has(t)&&void 0!==a&&!("error"in a)&&(null==n.openAt||Date.now()>=new Date(n.openAt).getTime())&&this.watchDetails.set(t,n),n}catch(i){return void this.logger.warn("Auction configuration read failed; rendering the live block alone",{auctionId:t,error:i})}}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;if(!b(this.lastStatusCache.get(t))){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}startWatching(t,i){this.stopWatching(t),this.hasSuccessfulReadThisSession.set(t,!1),this.lastStatusCache.delete(t),this.lastSettleAtMs.delete(t),i?this.degradedHandlers.set(t,i):this.degradedHandlers.delete(t);const e=this.desiredPollBaseMs(t);this.logger.info("Starting auction status polling",{auctionId:t,intervalMs:e}),this.installScheduler(t,e),this.events.emit("auction:watching-started",{auctionId:t,intervalMs:e})}installScheduler(t,i){const e=this.lifecycleGeneration,s=this.bumpMonitorGeneration(t),n=c({baseMs:i,suppressBackoff:()=>this.shouldSuppressBackoff(t),onDegradedChange:this.degradedHandlers.get(t),shouldContinue:()=>this.pollSchedulers.get(t)===n&&this.canApplyAsyncResult(t,e,s),run:async()=>{const i=this.getOperationGeneration(t);try{await this.doStatus(t,e,s,i)}catch(n){if(!this.canApplyAsyncResult(t,e,s,i))return;throw this.logger.error("Polling failed",{auctionId:t,error:n}),n}}});this.pollSchedulers.set(t,n),this.pollBaseMs.set(t,i),this.registerVisibilityPoke(t,n),n.start()}desiredPollBaseMs(t){const i=this.getTrackedParticipation(t)?.status??this.lastStatusCache.get(t)?.status;return(void 0===i||"not_bid"===i)&&!this.isInSettlingWindow(t)?this.WATCHING_POLL_MS:this.PARTICIPATING_POLL_MS}syncPollCadence(t){const i=this.pollSchedulers.get(t);if(!i)return;const e=this.desiredPollBaseMs(t);this.pollBaseMs.get(t)!==e&&(i.stop(),this.unregisterVisibilityPoke(t),this.logger.debug("Re-cadencing auction status polling",{auctionId:t,intervalMs:e}),this.installScheduler(t,e))}stopWatching(t){this.bumpMonitorGeneration(t),this.watchDetails.delete(t);const i=this.pollSchedulers.get(t);i&&(i.stop(),this.clearDutchStep(t),this.pollSchedulers.delete(t),this.pollBaseMs.delete(t),this.degradedHandlers.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&&!b(this.lastStatusCache.get(t)))}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))}startMonitoring(t,i,e,s){b(this.lastStatusCache.get(t))||(this.monitorRuntime.start(t,i,e),this.startWatching(t,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();for(const t of Array.from(this.dutchStepTimers.keys()))this.clearDutchStep(t);this.trackedParticipations.clear(),this.monitorGenerations.clear(),this.operationGenerations.clear(),this.auctionDetailsCache.clear(),this.watchDetails.clear(),this.pollBaseMs.clear(),this.degradedHandlers.clear(),this.monitorRuntime.clear()}}export{D as AuctionManagementModule};
|
|
@@ -4,6 +4,8 @@ import { ReadableAtom } from 'nanostores';
|
|
|
4
4
|
import { AuctionDisplayState } from '../experiences/distribution-monitor.types';
|
|
5
5
|
import { DistributionSummary } from '../experiences/journey.types';
|
|
6
6
|
import { CheckoutNextAction, GrantPaymentAffordance } from '../payment/types';
|
|
7
|
+
import { SelectionChoice, SelectionOptions } from '../selections/types';
|
|
8
|
+
import { AuctionBidInput } from './types';
|
|
7
9
|
/**
|
|
8
10
|
* Auction participation facts for one consumer.
|
|
9
11
|
*
|
|
@@ -14,6 +16,8 @@ import { CheckoutNextAction, GrantPaymentAffordance } from '../payment/types';
|
|
|
14
16
|
export interface AuctionSequenceConsumerState {
|
|
15
17
|
/** Server auction status for this consumer. */
|
|
16
18
|
status: AuctionConsumerStatus;
|
|
19
|
+
/** Recorded product and variant choice. */
|
|
20
|
+
selection?: SelectionChoice;
|
|
17
21
|
/** Auction participation id, when the server has created one. */
|
|
18
22
|
id?: string;
|
|
19
23
|
/** Distribution id this auction participation belongs to. */
|
|
@@ -121,6 +125,7 @@ export interface AuctionGranted {
|
|
|
121
125
|
* remaining affordances follow from this and nothing else.
|
|
122
126
|
*/
|
|
123
127
|
settlement: GrantedSettlement;
|
|
128
|
+
/** Current product and variant selection for this grant. */
|
|
124
129
|
selection: GrantedSelection;
|
|
125
130
|
pendingAction?: CheckoutNextAction;
|
|
126
131
|
}
|
|
@@ -148,7 +153,7 @@ export interface AuctionEnded {
|
|
|
148
153
|
* on the phases where they are legal, and each action may reject if the server
|
|
149
154
|
* has moved the sequence before the request is handled.
|
|
150
155
|
*/
|
|
151
|
-
export type AuctionSequenceView = AuctionScheduledView | AuctionEnterableView | AuctionParticipatingView | AuctionSettlingView | AuctionGrantedView | AuctionEndedView;
|
|
156
|
+
export type AuctionSequenceView = AuctionScheduledView | AuctionEnterableView | AuctionParticipatingView | AuctionSettlingView | AuctionGrantedView | AuctionGrantedAwaitingAssignmentView | AuctionGrantedSelectionRequiredView | AuctionEndedView;
|
|
152
157
|
/**
|
|
153
158
|
* Scheduled auction view rendered before bidding opens.
|
|
154
159
|
*/
|
|
@@ -178,14 +183,21 @@ export interface AuctionEnterableView {
|
|
|
178
183
|
};
|
|
179
184
|
/** Consumer auction facts before the first bid. */
|
|
180
185
|
consumer: AuctionSequenceConsumerState;
|
|
186
|
+
/** Live auction display facts: the price model, currency, and close time. */
|
|
187
|
+
display$: ReadableAtom<AuctionDisplayState>;
|
|
188
|
+
/** Load the current selection options. */
|
|
189
|
+
loadSelectionOptions(): Promise<SelectionOptions>;
|
|
181
190
|
/**
|
|
182
191
|
* Place the first auction bid.
|
|
183
192
|
*
|
|
184
|
-
* Legal only while this view is current. `amount
|
|
185
|
-
*
|
|
186
|
-
*
|
|
193
|
+
* Legal only while this view is current. An English auction requires `input.amount`, a decimal
|
|
194
|
+
* money string; a Dutch auction clinches at the price the server is showing and refuses an
|
|
195
|
+
* amount. The promise rejects if the input does not match the mechanism, if bidding is closed,
|
|
196
|
+
* or if the server rejects the bid.
|
|
197
|
+
* `input.selection` names the product this bid is for on a selection-enabled auction; the server
|
|
198
|
+
* rejects a missing required choice with SELECTION_REQUIRED.
|
|
187
199
|
*/
|
|
188
|
-
bid(
|
|
200
|
+
bid(input: AuctionBidInput): Promise<void>;
|
|
189
201
|
}
|
|
190
202
|
/**
|
|
191
203
|
* Participating auction view rendered while the consumer is bidding.
|
|
@@ -198,16 +210,21 @@ export interface AuctionParticipatingView {
|
|
|
198
210
|
mechanism: "auction";
|
|
199
211
|
/** Consumer auction participation facts. */
|
|
200
212
|
consumer: AuctionSequenceConsumerState;
|
|
201
|
-
/** Live auction display facts
|
|
213
|
+
/** Live auction display facts: the price model, currency, and close time. */
|
|
202
214
|
display$: ReadableAtom<AuctionDisplayState>;
|
|
215
|
+
/** Load the current selection options. */
|
|
216
|
+
loadSelectionOptions(): Promise<SelectionOptions>;
|
|
203
217
|
/**
|
|
204
218
|
* Place or raise an auction bid.
|
|
205
219
|
*
|
|
206
|
-
* Legal only while this view is current. `amount
|
|
207
|
-
*
|
|
208
|
-
*
|
|
220
|
+
* Legal only while this view is current. An English auction requires `input.amount`, a decimal
|
|
221
|
+
* money string at or above the price model's `minNextBid`; a Dutch auction clinches at the price
|
|
222
|
+
* the server is showing and refuses an amount. The promise rejects if the input does not match
|
|
223
|
+
* the mechanism, if bidding is closed, or if the server rejects the bid.
|
|
224
|
+
* `input.selection` names the product this bid is for on a selection-enabled auction; the server
|
|
225
|
+
* rejects a missing required choice with SELECTION_REQUIRED.
|
|
209
226
|
*/
|
|
210
|
-
bid(
|
|
227
|
+
bid(input: AuctionBidInput): Promise<void>;
|
|
211
228
|
}
|
|
212
229
|
/**
|
|
213
230
|
* Settling auction view rendered while the server computes the auction result.
|
|
@@ -240,8 +257,10 @@ interface AuctionGrantedViewBase {
|
|
|
240
257
|
consumer: AuctionSequenceConsumerState;
|
|
241
258
|
/** Admission credential issued by the server for this auction win. */
|
|
242
259
|
grant: AdmissionGrant;
|
|
243
|
-
/**
|
|
244
|
-
selection: GrantedSelection
|
|
260
|
+
/** Resolved product and variant choice. */
|
|
261
|
+
selection: Extract<GrantedSelection, {
|
|
262
|
+
status: "resolved";
|
|
263
|
+
}>;
|
|
245
264
|
}
|
|
246
265
|
/** Granted auction view whose actions follow the server-projected settlement arm. */
|
|
247
266
|
export type AuctionGrantedView = (AuctionGrantedViewBase & {
|
|
@@ -252,6 +271,10 @@ export type AuctionGrantedView = (AuctionGrantedViewBase & {
|
|
|
252
271
|
}>;
|
|
253
272
|
/** Claim the auction admission credential for checkout. */
|
|
254
273
|
claim(): AdmissionGrant;
|
|
274
|
+
/** Load the current selection options. */
|
|
275
|
+
loadSelectionOptions(): Promise<SelectionOptions>;
|
|
276
|
+
/** Submit a product and variant choice. */
|
|
277
|
+
select(choice: SelectionChoice): Promise<void>;
|
|
255
278
|
/** Embedded payment actions available before this tab claims the checkout. */
|
|
256
279
|
payment: GrantPaymentAffordance;
|
|
257
280
|
}) | (AuctionGrantedViewBase & {
|
|
@@ -262,6 +285,10 @@ export type AuctionGrantedView = (AuctionGrantedViewBase & {
|
|
|
262
285
|
}>;
|
|
263
286
|
/** Claim the auction admission credential; this tab has already claimed the checkout. */
|
|
264
287
|
claim(): AdmissionGrant;
|
|
288
|
+
/** Load the current selection options. */
|
|
289
|
+
loadSelectionOptions(): Promise<SelectionOptions>;
|
|
290
|
+
/** Submit a product and variant choice. */
|
|
291
|
+
select(choice: SelectionChoice): Promise<void>;
|
|
265
292
|
/** Absent after this tab claims the checkout. */
|
|
266
293
|
payment?: undefined;
|
|
267
294
|
}) | (AuctionGrantedViewBase & {
|
|
@@ -277,14 +304,10 @@ export type AuctionGrantedView = (AuctionGrantedViewBase & {
|
|
|
277
304
|
* phase.
|
|
278
305
|
*/
|
|
279
306
|
claim(): AdmissionGrant;
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
mode: "pre_auth";
|
|
285
|
-
}>;
|
|
286
|
-
/** Absent because a pre-authorized win has no checkout to claim against. */
|
|
287
|
-
claim?: undefined;
|
|
307
|
+
/** Load the current selection options. */
|
|
308
|
+
loadSelectionOptions(): Promise<SelectionOptions>;
|
|
309
|
+
/** Submit a product and variant choice. */
|
|
310
|
+
select(choice: SelectionChoice): Promise<void>;
|
|
288
311
|
});
|
|
289
312
|
/**
|
|
290
313
|
* Ended auction view rendered once the auction journey is terminal.
|
|
@@ -299,4 +322,50 @@ export interface AuctionEndedView {
|
|
|
299
322
|
/** Server-derived terminal outcome for the auction participation. */
|
|
300
323
|
outcome: SequenceOutcome;
|
|
301
324
|
}
|
|
325
|
+
/** Granted auction view waiting for a product assignment, with no available actions. */
|
|
326
|
+
export interface AuctionGrantedAwaitingAssignmentView {
|
|
327
|
+
/** Phase discriminator. */
|
|
328
|
+
phase: "granted";
|
|
329
|
+
/** Mechanism discriminator. */
|
|
330
|
+
mechanism: "auction";
|
|
331
|
+
/** Distribution associated with this grant. */
|
|
332
|
+
distribution: DistributionSummary & {
|
|
333
|
+
type: "auction";
|
|
334
|
+
};
|
|
335
|
+
/** Consumer participation associated with this grant. */
|
|
336
|
+
consumer: AuctionSequenceConsumerState;
|
|
337
|
+
/** Admission credential and its expiry. */
|
|
338
|
+
grant: AdmissionGrant;
|
|
339
|
+
/** How the grant settles. */
|
|
340
|
+
settlement: GrantedSettlement;
|
|
341
|
+
/** Assignment is not yet available. */
|
|
342
|
+
selection: Extract<GrantedSelection, {
|
|
343
|
+
status: "awaiting_assignment";
|
|
344
|
+
}>;
|
|
345
|
+
}
|
|
346
|
+
/** Granted auction view requiring a choice before checkout becomes available. */
|
|
347
|
+
export interface AuctionGrantedSelectionRequiredView {
|
|
348
|
+
/** Phase discriminator. */
|
|
349
|
+
phase: "granted";
|
|
350
|
+
/** Mechanism discriminator. */
|
|
351
|
+
mechanism: "auction";
|
|
352
|
+
/** Distribution associated with this grant. */
|
|
353
|
+
distribution: DistributionSummary & {
|
|
354
|
+
type: "auction";
|
|
355
|
+
};
|
|
356
|
+
/** Consumer participation associated with this grant. */
|
|
357
|
+
consumer: AuctionSequenceConsumerState;
|
|
358
|
+
/** Admission credential and its expiry. */
|
|
359
|
+
grant: AdmissionGrant;
|
|
360
|
+
/** How the grant settles. */
|
|
361
|
+
settlement: GrantedSettlement;
|
|
362
|
+
/** Product or variant choice still required. */
|
|
363
|
+
selection: Extract<GrantedSelection, {
|
|
364
|
+
status: "required";
|
|
365
|
+
}>;
|
|
366
|
+
/** Load the current selection options. */
|
|
367
|
+
loadSelectionOptions(): Promise<SelectionOptions>;
|
|
368
|
+
/** Submit a product and variant choice. */
|
|
369
|
+
select(choice: SelectionChoice): Promise<void>;
|
|
370
|
+
}
|
|
302
371
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
function t(t){return"
|
|
1
|
+
function t(t){return"winning"===t.status||"outbid"===t.status}export{t as isUnresolvedAuctionConsumer};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{isMoneyString as r,subtractMoney as e,isMoneyLessThanOrEqualTo as t}from"../core/money.js";function o(o,i){const{nextDropAt:n,priceDropAmount:c,priceDropIntervalSeconds:s,currentPrice:u}=o;if(!n||!c||!s||s<=0)return o;if(!r(u)||!r(c))return o;const a=o.floorPrice??"0";if(!r(a))return o;const N=Date.parse(n);if(Number.isNaN(N))return o;const p=Date.parse(o.settleAt),f=Number.isNaN(p)?i:Math.min(i,p),m=1e3*s;let D=u,d=N;for(;void 0!==d&&f>=d&&(Number.isNaN(p)||d<p);){const r=e(D,c);if(t(r,a)){D=a,d=void 0;break}D=r,d+=m,!Number.isNaN(p)&&d>=p&&(d=void 0)}return D===u&&d===N?o:{...o,currentPrice:D,nextDropAt:void 0===d?void 0:new Date(d).toISOString()}}export{o as advanceDutchPricing};
|
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* Public auction surface. The runtime `AuctionManagementModule`/`AuctionModule`/`AuctionParticipation`
|
|
3
3
|
* symbols from `./types` are internal and not exported here.
|
|
4
4
|
*/
|
|
5
|
-
export type { AuctionConsumerStatus, AuctionDetails,
|
|
5
|
+
export type { AuctionBidInput, AuctionConsumerStatus, AuctionDetails, AuctionLive, AuctionStatus, BidResult, } from './types';
|