@fanfare-io/fanfare-sdk-core 0.2.0 → 0.4.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 (53) hide show
  1. package/dist/appointments/appointment.driver.d.ts +44 -0
  2. package/dist/appointments/appointment.driver.js +1 -0
  3. package/dist/appointments/appointment.sequence.d.ts +194 -0
  4. package/dist/auctions/auction.driver.d.ts +46 -0
  5. package/dist/auctions/auction.driver.js +1 -0
  6. package/dist/auctions/auction.module.js +1 -1
  7. package/dist/auctions/auction.sequence.d.ts +263 -0
  8. package/dist/core/client.js +1 -1
  9. package/dist/draws/draw.driver.d.ts +45 -0
  10. package/dist/draws/draw.driver.js +1 -0
  11. package/dist/draws/draw.module.js +1 -1
  12. package/dist/draws/draw.sequence.d.ts +249 -0
  13. package/dist/experiences/distribution-monitor.types.d.ts +92 -2
  14. package/dist/experiences/experience.module.js +1 -1
  15. package/dist/experiences/journey-view.d.ts +8 -10
  16. package/dist/experiences/journey-view.js +1 -1
  17. package/dist/experiences/journey.d.ts +45 -20
  18. package/dist/experiences/journey.js +1 -1
  19. package/dist/experiences/journey.machine.d.ts +3 -20
  20. package/dist/experiences/journey.machine.js +1 -1
  21. package/dist/experiences/journey.types.d.ts +422 -217
  22. package/dist/experiences/public.d.ts +1 -1
  23. package/dist/experiences/sequence-driver.d.ts +93 -0
  24. package/dist/experiences/types.d.ts +185 -23
  25. package/dist/internals.d.ts +1 -1
  26. package/dist/internals.js +1 -1
  27. package/dist/queues/queue.driver.d.ts +45 -0
  28. package/dist/queues/queue.driver.js +1 -0
  29. package/dist/queues/queue.module.js +1 -1
  30. package/dist/queues/queue.sequence.d.ts +222 -0
  31. package/dist/state/capability-token-registry.d.ts +1 -0
  32. package/dist/state/capability-token-registry.js +1 -1
  33. package/dist/state/store.d.ts +5 -1
  34. package/dist/state/store.js +1 -1
  35. package/dist/storefront.d.ts +14 -24
  36. package/dist/storefront.js +1 -1
  37. package/dist/test-utils/harness-scenarios.d.ts +8 -1
  38. package/dist/test-utils/harness-scenarios.js +1 -1
  39. package/dist/test-utils/mock-journey.d.ts +15 -4
  40. package/dist/test-utils/mock-journey.js +1 -1
  41. package/dist/test-utils/mock-sdk.js +1 -1
  42. package/dist/test-utils/verification-scenarios.d.ts +9 -1
  43. package/dist/test-utils/verification-scenarios.js +1 -1
  44. package/dist/timed-releases/timed-release.driver.d.ts +45 -0
  45. package/dist/timed-releases/timed-release.driver.js +1 -0
  46. package/dist/timed-releases/timed-release.module.d.ts +6 -1
  47. package/dist/timed-releases/timed-release.module.js +1 -1
  48. package/dist/timed-releases/timed-release.sequence.d.ts +227 -0
  49. package/dist/timed-releases/types.d.ts +16 -1
  50. package/dist/types/distribution-type.d.ts +1 -1
  51. package/dist/version.d.ts +1 -1
  52. package/dist/version.js +1 -1
  53. package/package.json +3 -3
@@ -0,0 +1,44 @@
1
+ import { WritableAtom } from 'nanostores';
2
+ import { DistributionDisplayState, DistributionMonitor, MonitorUpdate } from '../experiences/distribution-monitor.types';
3
+ import { JourneyEvent, JourneyEventAudience, JourneyEventKind, JourneyEventSeverity, MechanismConsumerState, Participation, SequenceOutcome, SequenceState } from '../experiences/journey.types';
4
+ import { SequenceDriver, SequenceProjectionFacts } from '../experiences/sequence-driver';
5
+ import { AppointmentModule } from './types';
6
+ type AppointmentMonitorModule = AppointmentModule & Pick<DistributionMonitor, "startMonitoring" | "stopMonitoring">;
7
+ /**
8
+ * Narrow shell and module capabilities required by the appointment driver.
9
+ */
10
+ export interface AppointmentDriverDependencies {
11
+ /** Appointment module functions used by appointment actions and monitoring. */
12
+ appointments: Pick<AppointmentMonitorModule, "book" | "cancel" | "reschedule" | "startMonitoring" | "stopMonitoring" | "bumpMonitorGeneration">;
13
+ /** Serialize an appointment action with the shell's operation queue. */
14
+ runSerializedOperation(operation: () => Promise<void>): Promise<void>;
15
+ /** Read the current routed sequence before validating an appointment action. */
16
+ getCurrentSequence(): SequenceState | undefined;
17
+ /** Read the current shell-owned display state for cancellation details. */
18
+ getDisplayState(): DistributionDisplayState | null;
19
+ /** Commit a concrete next appointment sequence into the shell. */
20
+ commit(next: SequenceState, opts?: {
21
+ events?: JourneyEvent[];
22
+ }): void;
23
+ /** Apply shell-owned terminal lifecycle side effects. */
24
+ applyMonitorUpdate(update: MonitorUpdate): void;
25
+ /** Ensure the shell-owned display atom exists for this appointment participation. */
26
+ ensureDisplayAtom(participation: Participation): WritableAtom<DistributionDisplayState>;
27
+ /** Stop shell runtime monitoring and optionally clear display state. */
28
+ stopRuntimeMonitoring(clearDisplay?: boolean): void;
29
+ /** Append a shell-visible journey event to the snapshot. */
30
+ emitEvent(kind: JourneyEventKind, severity: JourneyEventSeverity, audience: JourneyEventAudience, message: string, detail?: Record<string, unknown>): void;
31
+ }
32
+ /**
33
+ * Create the appointment sequence driver for one journey instance.
34
+ */
35
+ export declare function createAppointmentDriver(deps: AppointmentDriverDependencies): SequenceDriver;
36
+ /**
37
+ * Project appointment server facts into an appointment sequence state.
38
+ */
39
+ export declare function projectAppointmentSequence(facts: SequenceProjectionFacts): SequenceState;
40
+ /**
41
+ * Return the appointment consumer state that represents a terminal outcome.
42
+ */
43
+ export declare function withAppointmentTerminalConsumer(consumer: MechanismConsumerState | undefined, outcome?: SequenceOutcome): MechanismConsumerState | undefined;
44
+ export {};
@@ -0,0 +1 @@
1
+ import{createError as t}from"../core/errors.js";function i(t){return new o(t)}function e(i){const{distribution:e,consumer:n,waitlist:o,outcome:a}=i;if(!e)return{phase:"unavailable"};const r=function(i){if("appointment"!==i.type)throw t.validationError("Appointment driver received a non-appointment distribution");return i}(e),p=n&&"appointment"===n.mechanism?function(t){const{mechanism:i,...e}=t;return e}(n):function(t){return{status:"not_booked",distributionId:t.id}}(e),d=a??function(t){return"completed"===t.status?{type:"completed"}:"cancelled"===t.status?{type:"cancelled"}:"no_show"===t.status?{type:"no_show"}:void 0}(p);if(d)return s(r,p,d);if(function(t){return"booked"===t.status||"checked_in"===t.status}(p))return{phase:"participating",mechanism:"appointment",distribution:r,consumer:p};if("scheduled"===r.lifecycle){const t=o&&o.waitlistId===r.waitlistId?o:r.waitlistId?{waitlistId:r.waitlistId,status:"not_waitlisted"}:void 0;return{phase:"scheduled",mechanism:"appointment",distribution:r,...t?{waitlist:t}:{}}}return"open"===r.lifecycle?{phase:"enterable",mechanism:"appointment",distribution:r,consumer:p}:s(r,p,{type:"closed"})}function n(t,i){return t&&"appointment"===t.mechanism&&i?"cancelled"===i.type?{...t,status:"cancelled"}:"no_show"===i.type?{...t,status:"no_show"}:"completed"===i.type?{...t,status:"completed"}:{...t,status:"not_booked"}:t}class o{constructor(t){this.deps=t,this.mechanism="appointment"}project(t){return e(t)}buildView(i,e){if("unavailable"===i.phase)return{phase:"unavailable",reason:i.reason};const n=function(i){if(!("mechanism"in i)||"appointment"!==i.mechanism)throw t.validationError("Appointment driver received a non-appointment sequence");return i}(i);switch(n.phase){case"scheduled":return{phase:"scheduled",mechanism:"appointment",distribution:n.distribution,startsAt:n.distribution.startsAt??n.distribution.opensAt,waitlist:n.waitlist?{status:n.waitlist.status,join:()=>e.joinWaitlist(),leave:()=>e.exitWaitlist()}:void 0};case"enterable":return{phase:"enterable",mechanism:"appointment",distribution:n.distribution,consumer:n.consumer,book:(t,i)=>this.book(t,i)};case"participating":return{phase:"participating",mechanism:"appointment",consumer:n.consumer,display$:e.displayAtom,cancel:t=>this.cancel(t),reschedule:(t,i)=>this.reschedule(t,i)};case"ended":return{phase:"ended",mechanism:"appointment",consumer:n.consumer,outcome:n.outcome}}}startMonitoring(t){this.activeMonitorId=t.participation.id,this.deps.appointments.startMonitoring(t.participation.id,{displayAtom:t.displayAtom},t.onUpdate)}stopMonitoring(){this.activeMonitorId&&(this.deps.appointments.stopMonitoring(this.activeMonitorId),this.activeMonitorId=void 0)}async book(t,i){await this.deps.runSerializedOperation(async()=>{const n=this.currentAppointmentSequence("enterable","No open appointment to book").distribution,o=await this.deps.appointments.book(n.id,t,i);this.deps.appointments.bumpMonitorGeneration(n.id),this.writeBookingDisplay(n.id,o),this.deps.commit(e({distribution:n,consumer:{mechanism:"appointment",status:"booked",id:n.id,distributionId:n.id,slotId:o.slotId,locationId:o.locationId}})),this.deps.emitEvent("sequence_change","success","user","Booked appointment slot",{appointmentId:n.id,slotId:t,locationId:i})})}async cancel(i){await this.deps.runSerializedOperation(async()=>{const e=this.currentAppointmentSequence("participating"),n=e.consumer,o=this.deps.getDisplayState(),s=o&&"appointment"===o.type?o.booking:void 0;if(!s)throw t.validationError("No booking details available to cancel");const a=n.distributionId??n.id??e.distribution.id;await this.deps.appointments.cancel(a,s.slotId,s.locationId??"",i),this.deps.stopRuntimeMonitoring(!0),this.deps.applyMonitorUpdate({type:"ended",outcome:{type:"cancelled",reason:i}})})}async reschedule(t,i){await this.deps.runSerializedOperation(async()=>{const e=this.currentAppointmentSequence("participating"),n=e.consumer,o=n.distributionId??n.id??e.distribution.id,s=await this.deps.appointments.reschedule(o,t,i);this.deps.appointments.bumpMonitorGeneration(o),this.writeBookingDisplay(o,s),this.deps.emitEvent("sequence_change","info","user","Rescheduled appointment",{appointmentId:o,newSlotId:t,newLocationId:i})})}writeBookingDisplay(t,i){const e=this.deps.ensureDisplayAtom({id:t,type:"appointment"}),n=Math.max(0,Date.parse(i.startTime)-Date.now());e.set({type:"appointment",booking:{slotId:i.slotId,locationId:i.locationId,locationName:i.locationName,joinUrl:i.joinUrl,startTime:i.startTime,endTime:i.endTime,confirmationCode:i.confirmationCode,consumerStatus:i.consumerStatus},msUntilStart:n})}currentAppointmentSequence(i,e="Not participating in an appointment"){const n=this.deps.getCurrentSequence();if(!n||n.phase!==i||!("mechanism"in n)||"appointment"!==n.mechanism)throw t.validationError(e);return n}}function s(t,i,e){return{phase:"ended",mechanism:"appointment",distribution:t,consumer:i,outcome:{...e,distributionType:e.distributionType??t.type,distributionId:e.distributionId??t.id,at:e.at??/* @__PURE__ */(new Date).toISOString()}}}export{i as createAppointmentDriver,e as projectAppointmentSequence,n as withAppointmentTerminalConsumer};
@@ -0,0 +1,194 @@
1
+ import { AppointmentConsumerStatus, SequenceOutcome } from '@fanfare-io/fanfare-sdk-contracts/consumer-me';
2
+ import { ReadableAtom } from 'nanostores';
3
+ import { AppointmentDisplayState } from '../experiences/distribution-monitor.types';
4
+ import { DistributionSummary, WaitlistAttachment, WaitlistAttachmentView } from '../experiences/journey.types';
5
+ /**
6
+ * Appointment booking facts for one consumer.
7
+ *
8
+ * The server is authoritative for every field. Timestamps are ISO 8601 strings.
9
+ * Slot and location ids are server identifiers. Optional fields are present only
10
+ * after the matching appointment lifecycle event has occurred.
11
+ */
12
+ export interface AppointmentSequenceConsumerState {
13
+ /** Server appointment status for this consumer. */
14
+ status: AppointmentConsumerStatus;
15
+ /** Appointment booking or participation id, when the server has created one. */
16
+ id?: string;
17
+ /** Distribution id this appointment booking belongs to. */
18
+ distributionId?: string;
19
+ /** Booked slot id from the appointment service. */
20
+ slotId?: string;
21
+ /** Booked location id from the appointment service. */
22
+ locationId?: string;
23
+ /** ISO 8601 timestamp for when the appointment was booked. */
24
+ bookedAt?: string;
25
+ /** ISO 8601 timestamp for when the consumer checked in. */
26
+ checkedInAt?: string;
27
+ /** ISO 8601 timestamp for when the appointment completed. */
28
+ completedAt?: string;
29
+ /** ISO 8601 timestamp for when the appointment was cancelled. */
30
+ cancelledAt?: string;
31
+ /** ISO 8601 timestamp for when the consumer was marked no-show. */
32
+ noShowAt?: string;
33
+ }
34
+ /**
35
+ * An appointment sequence as seen by one consumer.
36
+ *
37
+ * Lifecycle: scheduled -> enterable -> participating -> ended. Appointment
38
+ * sequences never have `settling` or `granted` phases; booking creates the
39
+ * participation and terminal outcomes are observed directly.
40
+ */
41
+ export type AppointmentSequence = AppointmentScheduled | AppointmentEnterable | AppointmentParticipating | AppointmentEnded;
42
+ /**
43
+ * The appointment exists but booking has not opened yet.
44
+ */
45
+ export interface AppointmentScheduled {
46
+ /** Mechanism discriminator for appointment narrowing. */
47
+ mechanism: "appointment";
48
+ /** Phase discriminator; scheduled appointments cannot be booked yet. */
49
+ phase: "scheduled";
50
+ /** Appointment distribution facts from the journey projector. */
51
+ distribution: DistributionSummary & {
52
+ type: "appointment";
53
+ };
54
+ /** Scheduled-only waitlist attachment, cleared when the appointment opens. */
55
+ waitlist?: WaitlistAttachment;
56
+ }
57
+ /**
58
+ * Booking is open and this consumer has not booked an appointment.
59
+ */
60
+ export interface AppointmentEnterable {
61
+ /** Mechanism discriminator for appointment narrowing. */
62
+ mechanism: "appointment";
63
+ /** Phase discriminator; `book()` is legal on the matching view. */
64
+ phase: "enterable";
65
+ /** Open appointment distribution facts from the journey projector. */
66
+ distribution: DistributionSummary & {
67
+ type: "appointment";
68
+ };
69
+ /** Consumer appointment facts before booking. */
70
+ consumer: AppointmentSequenceConsumerState;
71
+ }
72
+ /**
73
+ * The consumer has booked an appointment and may manage the booking.
74
+ */
75
+ export interface AppointmentParticipating {
76
+ /** Mechanism discriminator for appointment narrowing. */
77
+ mechanism: "appointment";
78
+ /** Phase discriminator; appointment display is live while participating. */
79
+ phase: "participating";
80
+ /** Appointment distribution facts from the journey projector. */
81
+ distribution: DistributionSummary & {
82
+ type: "appointment";
83
+ };
84
+ /** Consumer appointment booking facts. */
85
+ consumer: AppointmentSequenceConsumerState;
86
+ }
87
+ /**
88
+ * The appointment journey is terminal for this consumer.
89
+ */
90
+ export interface AppointmentEnded {
91
+ /** Mechanism discriminator for appointment narrowing. */
92
+ mechanism: "appointment";
93
+ /** Phase discriminator; no appointment actions are legal after this phase. */
94
+ phase: "ended";
95
+ /** Appointment distribution facts from the journey projector. */
96
+ distribution: DistributionSummary & {
97
+ type: "appointment";
98
+ };
99
+ /** Consumer appointment facts at the terminal outcome. */
100
+ consumer: AppointmentSequenceConsumerState;
101
+ /** Server-derived terminal outcome for the appointment booking. */
102
+ outcome: SequenceOutcome;
103
+ }
104
+ /**
105
+ * Appointment views exposed by `JourneyView.sequence`.
106
+ *
107
+ * The view lifecycle mirrors the appointment sequence lifecycle. Actions exist
108
+ * only on the phases where they are legal, and each action may reject if the
109
+ * server has moved the sequence before the request is handled.
110
+ */
111
+ export type AppointmentSequenceView = AppointmentScheduledView | AppointmentEnterableView | AppointmentParticipatingView | AppointmentEndedView;
112
+ /**
113
+ * Scheduled appointment view rendered before booking opens.
114
+ */
115
+ export interface AppointmentScheduledView {
116
+ /** Phase discriminator; no appointment action is legal yet. */
117
+ phase: "scheduled";
118
+ /** Mechanism discriminator for appointment rendering. */
119
+ mechanism: "appointment";
120
+ /** Appointment distribution facts from the journey projector. */
121
+ distribution: DistributionSummary & {
122
+ type: "appointment";
123
+ };
124
+ /** ISO 8601 opening timestamp chosen from the distribution start/open time. */
125
+ startsAt?: string;
126
+ /** Scheduled-only waitlist actions; this attachment never grants access. */
127
+ waitlist?: WaitlistAttachmentView;
128
+ }
129
+ /**
130
+ * Enterable appointment view rendered when the consumer can book a slot.
131
+ */
132
+ export interface AppointmentEnterableView {
133
+ /** Phase discriminator; `book()` is legal on this view. */
134
+ phase: "enterable";
135
+ /** Mechanism discriminator for appointment rendering. */
136
+ mechanism: "appointment";
137
+ /** Open appointment distribution facts from the journey projector. */
138
+ distribution: DistributionSummary & {
139
+ type: "appointment";
140
+ };
141
+ /** Consumer appointment facts before booking. */
142
+ consumer: AppointmentSequenceConsumerState;
143
+ /**
144
+ * Book an appointment slot.
145
+ *
146
+ * Legal only while this view is current. `slotId` is required and
147
+ * `locationId` is optional when the appointment has a default location. The
148
+ * promise rejects if booking is closed, the slot is invalid, or the server
149
+ * rejects the booking.
150
+ */
151
+ book(slotId: string, locationId?: string): Promise<void>;
152
+ }
153
+ /**
154
+ * Participating appointment view rendered after the consumer has booked.
155
+ */
156
+ export interface AppointmentParticipatingView {
157
+ /** Phase discriminator; `cancel()` and `reschedule()` are legal on this view. */
158
+ phase: "participating";
159
+ /** Mechanism discriminator for appointment rendering. */
160
+ mechanism: "appointment";
161
+ /** Consumer appointment booking facts. */
162
+ consumer: AppointmentSequenceConsumerState;
163
+ /** Live appointment display facts, including slot, location, and check-in state. */
164
+ display$: ReadableAtom<AppointmentDisplayState>;
165
+ /**
166
+ * Cancel the booked appointment.
167
+ *
168
+ * Legal only while this view is current. The optional reason is forwarded to
169
+ * the server. The promise rejects if the booking cannot be cancelled or the
170
+ * server rejects the cancellation.
171
+ */
172
+ cancel(reason?: string): Promise<void>;
173
+ /**
174
+ * Move the booking to another slot and optional location.
175
+ *
176
+ * Legal only while this view is current. The promise rejects if the new slot
177
+ * is unavailable, the booking can no longer be rescheduled, or the server
178
+ * rejects the request.
179
+ */
180
+ reschedule(newSlotId: string, newLocationId?: string): Promise<void>;
181
+ }
182
+ /**
183
+ * Ended appointment view rendered once the booking is terminal.
184
+ */
185
+ export interface AppointmentEndedView {
186
+ /** Phase discriminator; no appointment action is legal after this phase. */
187
+ phase: "ended";
188
+ /** Mechanism discriminator for appointment rendering. */
189
+ mechanism: "appointment";
190
+ /** Consumer appointment facts at the terminal outcome. */
191
+ consumer: AppointmentSequenceConsumerState;
192
+ /** Server-derived terminal outcome for the appointment booking. */
193
+ outcome: SequenceOutcome;
194
+ }
@@ -0,0 +1,46 @@
1
+ import { WritableAtom } from 'nanostores';
2
+ import { DistributionDisplayState, DistributionMonitor, MonitorUpdate } from '../experiences/distribution-monitor.types';
3
+ import { DistributionSummary, JourneyEvent, JourneyEventAudience, JourneyEventKind, JourneyEventSeverity, MechanismConsumerState, Participation, SequenceOutcome, SequenceState } from '../experiences/journey.types';
4
+ import { SequenceDriver, SequenceProjectionFacts } from '../experiences/sequence-driver';
5
+ import { AuctionModule } from './types';
6
+ type AuctionMonitorModule = AuctionModule & Pick<DistributionMonitor, "startMonitoring" | "stopMonitoring">;
7
+ /**
8
+ * Narrow shell and module capabilities required by the auction driver.
9
+ */
10
+ export interface AuctionDriverDependencies {
11
+ /** Auction module functions used by auction actions and monitoring. */
12
+ auctions: Pick<AuctionMonitorModule, "enter" | "leave" | "status" | "bid" | "startMonitoring" | "stopMonitoring">;
13
+ /** Serialize an auction action with the shell's operation queue. */
14
+ runSerializedOperation(operation: () => Promise<void>): Promise<void>;
15
+ /** Read the current routed sequence before validating an auction action. */
16
+ getCurrentSequence(): SequenceState | undefined;
17
+ /** Commit a concrete next auction sequence into the shell. */
18
+ commit(next: SequenceState, opts?: {
19
+ events?: JourneyEvent[];
20
+ }): void;
21
+ /** Apply shell-owned grant and terminal lifecycle side effects. */
22
+ applyMonitorUpdate(update: MonitorUpdate): void;
23
+ /** Ensure the shell-owned display atom exists for this auction participation. */
24
+ ensureDisplayAtom(participation: Participation): WritableAtom<DistributionDisplayState>;
25
+ /** Stop shell runtime monitoring and optionally clear display state. */
26
+ stopRuntimeMonitoring(clearDisplay?: boolean): void;
27
+ /** Append a shell-visible journey event to the snapshot. */
28
+ emitEvent(kind: JourneyEventKind, severity: JourneyEventSeverity, audience: JourneyEventAudience, message: string, detail?: Record<string, unknown>): void;
29
+ }
30
+ /**
31
+ * Create the auction sequence driver for one journey instance.
32
+ */
33
+ export declare function createAuctionDriver(deps: AuctionDriverDependencies): SequenceDriver;
34
+ /**
35
+ * Project auction server facts into an auction sequence state.
36
+ */
37
+ export declare function projectAuctionSequence(facts: SequenceProjectionFacts): SequenceState;
38
+ /**
39
+ * Return the auction consumer state that represents an issued grant.
40
+ */
41
+ export declare function withAuctionGrantedConsumer(consumer: MechanismConsumerState | undefined, distribution: DistributionSummary): MechanismConsumerState;
42
+ /**
43
+ * Return the auction consumer state that represents a terminal outcome.
44
+ */
45
+ export declare function withAuctionTerminalConsumer(consumer: MechanismConsumerState | undefined, outcome?: SequenceOutcome): MechanismConsumerState | undefined;
46
+ export {};
@@ -0,0 +1 @@
1
+ import{createError as i}from"../core/errors.js";import{isMoneyString as t,addMoney as e}from"../core/money.js";function n(i){return new r(i)}function s(t){const{distribution:e,consumer:n,waitlist:s,grant:a,outcome:o}=t;if(!e)return{phase:"unavailable"};const r=function(t){if("auction"!==t.type)throw i.validationError("Auction driver received a non-auction distribution");return t}(e),d=n&&"auction"===n.mechanism?function(i){const{mechanism:t,...e}=i;return e}(n):u(e);if(a)return"completed"===o?.type?c(r,d,o):a.expiresAt&&Date.now()>new Date(a.expiresAt).getTime()?"open"===r.lifecycle?{phase:"enterable",mechanism:"auction",distribution:r,consumer:u(r)}:c(r,d,{type:"expired"}):{phase:"granted",mechanism:"auction",distribution:r,consumer:d,grant:a};if(!o&&"auction"===n?.mechanism&&"closed"===r.lifecycle)return c(r,d,{type:"lost"});const h=o??function(i){return"lost"===i.status?{type:"lost"}:void 0}(d);if(h)return c(r,d,h);if(function(i){return"bidding"===i.status||"winning"===i.status||"outbid"===i.status}(d))return{phase:"participating",mechanism:"auction",distribution:r,consumer:d};if("scheduled"===r.lifecycle){const i=s&&s.waitlistId===r.waitlistId?s:r.waitlistId?{waitlistId:r.waitlistId,status:"not_waitlisted"}:void 0;return{phase:"scheduled",mechanism:"auction",distribution:r,...i?{waitlist:i}:{}}}return"open"===r.lifecycle?{phase:"enterable",mechanism:"auction",distribution:r,consumer:d}:"settling"===r.lifecycle?{phase:"settling",mechanism:"auction",distribution:r,consumer:d}:c(r,d,{type:"closed"})}function a(i,t){return"auction"===i?.mechanism?{...i,status:"won"}:{mechanism:"auction",status:"won",id:t.id,distributionId:t.id}}function o(i,t){return i&&"auction"===i.mechanism&&t?"won"===t.type?{...i,status:"won"}:{...i,status:"lost"}:i}class r{constructor(i){this.deps=i,this.mechanism="auction"}project(i){return s(i)}buildView(t,e){if("unavailable"===t.phase)return{phase:"unavailable",reason:t.reason};const n=function(t){if(!("mechanism"in t)||"auction"!==t.mechanism)throw i.validationError("Auction driver received a non-auction sequence");return t}(t);switch(n.phase){case"scheduled":return{phase:"scheduled",mechanism:"auction",distribution:n.distribution,startsAt:n.distribution.startsAt??n.distribution.opensAt,waitlist:n.waitlist?{status:n.waitlist.status,join:()=>e.joinWaitlist(),leave:()=>e.exitWaitlist()}:void 0};case"enterable":return{phase:"enterable",mechanism:"auction",distribution:n.distribution,consumer:n.consumer,bid:i=>this.bid(i)};case"participating":return{phase:"participating",mechanism:"auction",consumer:n.consumer,display$:e.displayAtom,bid:i=>this.bid(i),leave:()=>this.leave()};case"settling":return{phase:"settling",mechanism:"auction",distribution:n.distribution,consumer:n.consumer};case"granted":return{phase:"granted",mechanism:"auction",distribution:n.distribution,consumer:n.consumer,grant:n.grant,claim:()=>e.claim()};case"ended":return{phase:"ended",mechanism:"auction",consumer:n.consumer,outcome:n.outcome}}}startMonitoring(i){this.activeMonitorId=i.participation.id,this.deps.auctions.startMonitoring(i.participation.id,{displayAtom:i.displayAtom},i.onUpdate)}stopMonitoring(){this.activeMonitorId&&(this.deps.auctions.stopMonitoring(this.activeMonitorId),this.activeMonitorId=void 0)}async enter(){await this.deps.runSerializedOperation(async()=>{await this.enterWithinOperation()})}async enterWithinOperation(){const i=this.currentAuctionSequence("enterable","No open auction to enter").distribution;await this.deps.auctions.enter(i.id,void 0),await this.deps.auctions.status(i.id);const t={mechanism:"auction",status:"bidding",id:e=i.id,distributionId:e};var e;this.deps.ensureDisplayAtom({id:i.id,type:"auction"}),this.deps.commit(s({distribution:i,consumer:t})),this.deps.emitEvent("sequence_change","success","user","Entered auction",{distributionId:i.id})}async leave(){await this.deps.runSerializedOperation(async()=>{await this.leaveWithinOperation()})}async leaveWithinOperation(){const i=this.currentAuctionSequence("participating"),t=i.consumer.distributionId??i.consumer.id??i.distribution.id;await this.deps.auctions.leave(t),this.deps.stopRuntimeMonitoring(!0),this.deps.applyMonitorUpdate({type:"ended",outcome:{type:"left"}}),this.deps.emitEvent("sequence_change","info","user","Left participation",{type:"auction"})}async bid(i){await this.deps.runSerializedOperation(async()=>{const n=this.currentAuctionSequenceForBid(),a=n.consumer.distributionId??n.consumer.id??n.distribution.id,o=await this.deps.auctions.bid(a,i),r="winning"===o.status?"winning":"outbid"===o.status?"outbid":"bidding",u="auction"===n.distribution.details?.type?n.distribution.details:void 0,c=u?.bidIncrement,d=o.highestBid&&c&&t(o.highestBid)&&t(c)?e(o.highestBid,c):void 0,h=this.deps.ensureDisplayAtom({id:a,type:"auction"});"enterable"===n.phase&&this.deps.commit(s({distribution:n.distribution,consumer:{mechanism:"auction",status:r,id:a,distributionId:a,currentBid:i,highestBid:o.highestBid}})),h.set({type:"auction",currentBid:i,highestBid:o.highestBid,bidCount:o.bidCount,status:r,currencyCode:u?.currencyCode,bidIncrement:c,minNextBid:d,closeAt:u?.closeAt??u?.endsAt})})}currentAuctionSequence(t,e="Not participating in an auction"){const n=this.deps.getCurrentSequence();if(!n||n.phase!==t||!("mechanism"in n)||"auction"!==n.mechanism)throw i.validationError(e);return n}currentAuctionSequenceForBid(){const t=this.deps.getCurrentSequence();if(!t||"enterable"!==t.phase&&"participating"!==t.phase||!("mechanism"in t)||"auction"!==t.mechanism)throw i.validationError("No auction is available to bid on");return t}}function u(i){return{status:"not_bid",distributionId:i.id}}function c(i,t,e){return{phase:"ended",mechanism:"auction",distribution:i,consumer:t,outcome:{...e,distributionType:e.distributionType??i.type,distributionId:e.distributionId??i.id,at:e.at??/* @__PURE__ */(new Date).toISOString()}}}export{n as createAuctionDriver,s as projectAuctionSequence,a as withAuctionGrantedConsumer,o as withAuctionTerminalConsumer};
@@ -1 +1 @@
1
- import{AuctionBidderStateResponseSchema as t,AuctionHighestBidResponseSchema as i,AuctionBidHistoryResponseSchema as e}from"@fanfare-io/fanfare-sdk-contracts/auction";import{createError as n}from"../core/errors.js";import{getLogger as a}from"../core/logger.js";import{isMoneyString as s,addMoney as o,isMoneyGreaterThan as r,normalizeMoney as d,isMoneyLessThanOrEqualTo as u,subtractMoney as c}from"../core/money.js";import{parseResponse as h}from"../core/parse-response.js";import{DistributionMonitorRuntime as l}from"../experiences/distribution-monitor.runtime.js";import{getEventBus as g}from"../state/events.js";import{getSDKStore as p}from"../state/store.js";class m{constructor(t){this.logger=a(),this.events=g(),this.trackedParticipations=/* @__PURE__ */new Map,this.inFlightRequests=/* @__PURE__ */new Map,this.pollingIntervals=/* @__PURE__ */new Map,this.lastStatusCache=/* @__PURE__ */new Map,this.autoRebidConfigs=/* @__PURE__ */new Map,this.monitorRuntime=new l,this.bidHistoryCache=/* @__PURE__ */new Map,this.auctionDetailsCache=/* @__PURE__ */new Map,this.monitorGenerations=/* @__PURE__ */new Map,this.operationGenerations=/* @__PURE__ */new Map,this.diagnosticEmissionIds=/* @__PURE__ */new Set,this.BID_HISTORY_CACHE_TTL=5e3,this.AUCTION_DETAILS_CACHE_TTL=3e4,this.destroyed=!1,this.lifecycleGeneration=0,this.http=t}get store(){return p()}computeMinNextBid(t,i){if(i&&s(t)&&s(i))return o(t,i)}async getAuctionDetailsCached(t){const i=this.auctionDetailsCache.get(t);if(i&&Date.now()-i.fetchedAt<this.AUCTION_DETAILS_CACHE_TTL)return i.details;const e=await this.http.get(`/auctions/${t}`);return this.auctionDetailsCache.set(t,{details:e,fetchedAt:Date.now()}),e}getTrackedParticipation(t){return this.trackedParticipations.get(t)}getAuctionCloseAt(t){return t.closeAt??t.settleAt}buildDisplayState(t,{status:i,highestBid:e,currentBid:n,bidCount:a}){return{highestBid:e,currentBid:n,bidCount:a,closeAt:this.getAuctionCloseAt(t),status:i,currencyCode:t.currencyCode,minNextBid:e?this.computeMinNextBid(e,t.minBidIncrement):void 0,bidIncrement:t.minBidIncrement??void 0}}emitTransitionEvents(t){for(const i of t)this.events.emit(i.type,i.payload)}applyTransition(t,i){i.trackedParticipation?this.trackedParticipations.set(t,i.trackedParticipation):this.trackedParticipations.delete(t);const e=this.monitorRuntime.getDisplayAtom(t);e&&i.displayState&&e.set({type:"auction",...i.displayState}),this.emitTransitionEvents(i.events),i.diagnosticError&&!this.diagnosticEmissionIds.has(t)?(this.diagnosticEmissionIds.add(t),this.logger.error("Auction monitoring diagnostic",{auctionId:t,error:i.diagnosticError}),this.events.emit("auction:error",{auctionId:t,error:i.diagnosticError})):i.diagnosticError||this.diagnosticEmissionIds.delete(t),i.monitorUpdate&&this.monitorRuntime.notify(t,i.monitorUpdate),i.stopWatching&&this.stopWatching(t),i.autoRebidStatus&&this.handleAutoRebid(t,i.autoRebidStatus)}checkOutbidConditions(t,i,e){if("winning"===t.status&&"outbid"===i.status)return!0;if(e?.currentBid&&i.highestBid){const n=e.currentBid,a=i.highestBid,o=t.highestBid||"0";if(s(n)&&s(a)&&s(o))return r(a,o)&&r(a,n)&&"winning"!==i.status}return"outbid"!==t.status&&"outbid"===i.status}buildBidTransition(t,i,e,n,a){const s="winning"===i.status?"winning":"outbid"===i.status?"outbid":"bidding";return{trackedParticipation:{auctionId:t,enteredAt:a?.enteredAt??/* @__PURE__ */(new Date).toISOString(),status:s,currentBid:e,highestBid:i.highestBid,bidCount:i.bidCount,lastBidAt:/* @__PURE__ */(new Date).toISOString(),metadata:a?.metadata,fingerprint:a?.fingerprint},displayState:this.buildDisplayState(n,{status:i.status,highestBid:i.highestBid,currentBid:e,bidCount:i.bidCount}),events:[{type:"auction:bid-placed",payload:{auctionId:t,amount:e,status:i.status}},..."winning"===i.status?[{type:"auction:winning",payload:{auctionId:t,amount:e,highestBid:i.highestBid}}]:[],..."outbid"===i.status?[{type:"auction:outbid",payload:{auctionId:t,yourBid:e,highestBid:i.highestBid}}]:[]],monitorUpdate:null,stopWatching:!1,autoRebidStatus:null}}buildStatusTransition(t,i,e,{previousParticipation:n,previousStatus:a}){const s=i.currentBid??n?.currentBid,o=i.bidCount??n?.bidCount;if(i.timeRemaining<=0){const a="winning"===i.status;return a&&!i.admissionGrant?{trackedParticipation:{auctionId:t,enteredAt:n?.enteredAt??/* @__PURE__ */(new Date).toISOString(),status:"winning",currentBid:s,highestBid:i.highestBid,bidCount:o,metadata:n?.metadata,fingerprint:n?.fingerprint},displayState:this.buildDisplayState(e,{status:"winning",highestBid:i.highestBid,currentBid:s,bidCount:o}),events:[],monitorUpdate:null,stopWatching:!1,autoRebidStatus:null,diagnosticError:new Error("Auction resolved to terminal winning without admissionGrant")}:{trackedParticipation:null,displayState:this.buildDisplayState(e,{status:a?"won":"lost",highestBid:i.highestBid,currentBid:s,bidCount:o}),events:a?[{type:"auction:won",payload:{auctionId:t,winningBid:s??""}}]:[{type:"auction:lost",payload:{auctionId:t,highestBid:i.highestBid}}],monitorUpdate:a&&i.admissionGrant?{type:"granted",token:i.admissionGrant}:a?null:{type:"ended",outcome:{type:"lost"}},stopWatching:!0,autoRebidStatus:null}}const r={auctionId:t,enteredAt:n?.enteredAt??/* @__PURE__ */(new Date).toISOString(),status:i.status,currentBid:s,highestBid:i.highestBid,bidCount:o,metadata:n?.metadata,fingerprint:n?.fingerprint},d=[];let u=null;return a&&(this.checkOutbidConditions(a,i,n)&&(d.push({type:"auction:outbid",payload:{auctionId:t,yourBid:a.currentBid||n?.currentBid||"",highestBid:i.highestBid}}),u=i),"winning"!==a.status&&"winning"===i.status&&d.push({type:"auction:winning",payload:{auctionId:t,amount:s||"",highestBid:i.highestBid}}),a.highestBid!==i.highestBid&&"winning"!==i.status&&d.push({type:"auction:bid-updated",payload:{auctionId:t,highestBid:i.highestBid,bidCount:i.bidCount||0}})),{trackedParticipation:r,displayState:this.buildDisplayState(e,{status:i.status,highestBid:i.highestBid,currentBid:s,bidCount:o}),events:d,monitorUpdate:null,stopWatching:!1,autoRebidStatus:u}}buildMissingTransition(){return{trackedParticipation:null,displayState:{status:"lost"},events:[],monitorUpdate:{type:"ended",outcome:{type:"closed",reason:"not_participating"}},stopWatching:!0,autoRebidStatus:null}}async get(t){try{return await this.getAuctionDetailsCached(t)}catch(i){throw this.logger.error("Failed to get auction",{auctionId:t,error:i}),i}}async bid(e,a,o){try{const r=this.lifecycleGeneration;if(!s(a))throw n.validationError("Invalid bid amount format. Must be a numeric string (e.g., '100.00')");if(!this.store.session)throw n.unauthorized("Not authenticated. Call auth.guest() or auth.login() first");let u=this.bumpOperationGeneration(e),c=this.getTrackedParticipation(e);c||(this.logger.warn("Attempting to bid without entering auction first",{auctionId:e}),await this.enter(e,void 0,o),c=this.getTrackedParticipation(e),u=this.bumpOperationGeneration(e)),this.logger.info("Placing bid",{auctionId:e,amount:a});const l=`/auctions/${e}/bid`,g=`/auctions/${e}/highest-bid`,[p,m,b]=await Promise.all([this.http.post(l,{amount:a},o),this.http.get(g),this.getAuctionDetailsCached(e)]),y=h(t,p,{endpoint:l},{throwOnFailure:!0});if(!y.ok)throw y.error;const I=h(i,m,{endpoint:g},{throwOnFailure:!0});if(!I.ok)throw I.error;const f=y.value,B=I.value,w=d(B.amount),v={status:"WINNING"===f.status||"WON"===f.status?"winning":"OUTBID"===f.status||"BIDDING"===f.status||"LOST"===f.status?"outbid":"accepted",amount:a,highestBid:w,bidCount:f.bidCount??(c?.bidCount||0)+1};return this.canApplyAsyncResult(e,r,void 0,u)?(this.applyTransition(e,this.buildBidTransition(e,v,a,b,c)),v):v}catch(r){throw this.logger.error("Failed to place bid",{auctionId:e,amount:a,error:r}),this.events.emit("auction:error",{auctionId:e,error:r}),r}}async enter(t,i,e){try{const s=this.lifecycleGeneration,o=this.store.session;if(!o)throw n.unauthorized("Not authenticated. Call auth.guest() or auth.login() first");const r=this.bumpOperationGeneration(t);this.logger.info("Entering auction",{auctionId:t});try{await this.http.post(`/auctions/${t}/enter`,{metadata:i},e)}catch(a){if(404!==a?.status)throw a}if(!this.canApplyAsyncResult(t,s,void 0,r))return;this.trackedParticipations.set(t,{auctionId:t,enteredAt:/* @__PURE__ */(new Date).toISOString(),status:"watching",metadata:i,fingerprint:o.deviceFingerprint}),this.emitTransitionEvents([{type:"auction:entered",payload:{auctionId:t}}])}catch(a){throw this.logger.error("Failed to enter auction",{auctionId:t,error:a}),this.events.emit("auction:error",{auctionId:t,error:a}),a}}async leave(t,i){try{const e=this.lifecycleGeneration,n=this.bumpOperationGeneration(t);this.logger.info("Leaving auction",{auctionId:t}),this.stopMonitoring(t);try{await this.http.post(`/auctions/${t}/leave`,void 0,i)}catch{this.logger.debug("Leave endpoint not available",{auctionId:t})}if(!this.canApplyAsyncResult(t,e,void 0,n))return;this.trackedParticipations.delete(t),this.emitTransitionEvents([{type:"auction:left",payload:{auctionId:t}}])}catch(e){throw this.logger.error("Failed to leave auction",{auctionId:t,error:e}),this.events.emit("auction:error",{auctionId:t,error:e}),e}}async status(t){const i=this.inFlightRequests.get(t);if(i)return this.logger.debug("Returning existing in-flight request",{auctionId:t}),i;const e=this.lifecycleGeneration,n=this.bumpOperationGeneration(t),a=this.doStatus(t,e,void 0,n).finally(()=>{this.inFlightRequests.delete(t)});return this.inFlightRequests.set(t,a),a}async doStatus(e,n,a,s){try{const o=`/auctions/${e}/status`,r=`/auctions/${e}/highest-bid`,[u,c,l]=await Promise.all([this.http.get(o),this.http.get(r),this.getAuctionDetailsCached(e)]),g=h(t,u,{endpoint:o},{throwOnFailure:!0});if(!g.ok)throw g.error;const p=h(i,c,{endpoint:r},{throwOnFailure:!0});if(!p.ok)throw p.error;const m=g.value,b=p.value,y=this.getAuctionCloseAt(l),I=y?new Date(y).getTime():Number.NaN,f=d(b.amount),B=this.getTrackedParticipation(e),w=m.lastBidAmount??m.winningBidAmount;let v="watching";"WINNING"===m.status||"WON"===m.status?v="winning":"OUTBID"!==m.status&&"LOST"!==m.status&&"BIDDING"!==m.status||(v="outbid");const A={auctionId:e,status:v,currentBid:B?.currentBid??(w?d(w):void 0),highestBid:f,timeRemaining:Number.isFinite(I)?Math.max(0,I-Date.now()):0,bidCount:m.bidCount,admissionGrant:m.admissionGrant};if(!this.canApplyAsyncResult(e,n,a,s))return A;const C=this.buildStatusTransition(e,A,l,{previousParticipation:B,previousStatus:this.lastStatusCache.get(e)});return this.applyTransition(e,C),C.stopWatching?this.lastStatusCache.delete(e):this.lastStatusCache.set(e,A),this.events.emit("auction:status-updated",{auctionId:e,status:A}),A}catch(o){if(o&&"object"==typeof o&&"status"in o&&404===o.status){const t={auctionId:e,status:"watching",highestBid:"0.00",timeRemaining:0};if(!this.canApplyAsyncResult(e,n,a,s))return t;const i=this.buildMissingTransition();return this.applyTransition(e,i),this.lastStatusCache.delete(e),this.events.emit("auction:status-updated",{auctionId:e,status:t}),t}if(!this.canApplyAsyncResult(e,n,a,s))return this.lastStatusCache.get(e)??{auctionId:e,status:"watching",highestBid:"0.00",timeRemaining:0};throw this.logger.error("Failed to get auction status",{auctionId:e,error:o}),this.events.emit("auction:error",{auctionId:e,error:o}),o}}async getBidHistory(t){try{const i=this.bidHistoryCache.get(t);if(i&&Date.now()-i.fetchedAt<this.BID_HISTORY_CACHE_TTL)return this.logger.debug("Returning cached bid history",{auctionId:t}),i.bids;this.logger.info("Fetching bid history",{auctionId:t});const n=`/auctions/${t}/bids/history`,a=await this.http.get(n),s=h(e,a,{endpoint:n},{throwOnFailure:!0});if(!s.ok)throw s.error;const o=s.value.map(t=>({amount: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){this.stopWatching(t);const e=this.bumpMonitorGeneration(t);this.logger.info("Starting auction status polling",{auctionId:t,intervalMs:i});const n=this.getOperationGeneration(t);this.doStatus(t,this.lifecycleGeneration,e,n).catch(i=>{this.canApplyAsyncResult(t,this.lifecycleGeneration,e,n)&&this.logger.error("Initial status check failed",{auctionId:t,error:i})});const a=setInterval(()=>{const i=this.getOperationGeneration(t);this.doStatus(t,this.lifecycleGeneration,e,i).catch(n=>{this.canApplyAsyncResult(t,this.lifecycleGeneration,e,i)&&this.logger.error("Polling failed",{auctionId:t,error:n})})},i);this.pollingIntervals.set(t,a),this.events.emit("auction:watching-started",{auctionId:t,intervalMs:i})}stopWatching(t){this.bumpMonitorGeneration(t);const i=this.pollingIntervals.get(t);i&&(clearInterval(i),this.pollingIntervals.delete(t),this.lastStatusCache.delete(t),this.logger.info("Stopped watching auction",{auctionId:t}),this.events.emit("auction:watching-stopped",{auctionId:t}))}stopAllWatching(){for(const t of Array.from(this.pollingIntervals.keys()))this.stopWatching(t)}bumpMonitorGeneration(t){const i=(this.monitorGenerations.get(t)??0)+1;return this.monitorGenerations.set(t,i),i}getOperationGeneration(t){return this.operationGenerations.get(t)??0}bumpOperationGeneration(t){const i=this.getOperationGeneration(t)+1;return this.operationGenerations.set(t,i),i}canApplyAsyncResult(t,i,e,n){return!this.destroyed&&this.lifecycleGeneration===i&&((void 0===e||this.monitorGenerations.get(t)===e)&&(void 0===n||this.getOperationGeneration(t)===n))}enableAutoRebid(t,i,e){if(!s(i)||!s(e))throw n.validationError("Invalid monetary format. Must be numeric strings (e.g., '100.00')");if(u(i,"0")||u(e,"0"))throw n.validationError("Max bid and increment must be positive");if(r(e,i))throw n.validationError("Increment cannot be greater than max bid");const a=this.getTrackedParticipation(t);if(!a)throw n.validationError("Must enter auction before enabling auto-rebid");const o=d(a.currentBid),h=c(i,o);this.autoRebidConfigs.set(t,{enabled:!0,maxBid:i,increment:e,remainingBudget:h,rebidCount:0}),this.logger.info("Auto-rebid enabled",{auctionId:t,maxBid:i,increment:e,remainingBudget:h}),this.events.emit("auction:auto-rebid-enabled",{auctionId:t,maxBid:i,increment:e})}disableAutoRebid(t){const i=this.autoRebidConfigs.get(t);i&&(i.enabled=!1,this.logger.info("Auto-rebid disabled",{auctionId:t}),this.events.emit("auction:auto-rebid-disabled",{auctionId:t,finalRebidCount:i.rebidCount||0}))}getAutoRebidConfig(t){return this.autoRebidConfigs.get(t)}async handleAutoRebid(t,i){const e=this.autoRebidConfigs.get(t);if(e&&e.enabled)try{if(!s(i.highestBid)||!s(e.maxBid)||!s(e.increment))return void this.logger.error("Invalid auto-rebid amounts",{auctionId:t,config:e});const n=o(i.highestBid,e.increment);if(r(n,e.maxBid))return this.logger.info("Auto-rebid would exceed max bid, disabling",{auctionId:t,wouldBid:n,maxBid:e.maxBid}),this.disableAutoRebid(t),void this.events.emit("auction:auto-rebid-max-reached",{auctionId:t,maxBid:e.maxBid,wouldNeedToBid:n});this.logger.info("Placing auto-rebid",{auctionId:t,amount:n});const a=await this.bid(t,n);e.lastRebidAt=/* @__PURE__ */(new Date).toISOString(),e.rebidCount=(e.rebidCount||0)+1,e.remainingBudget=c(e.maxBid,n),this.events.emit("auction:auto-rebid-placed",{auctionId:t,amount:n,remainingBudget:e.remainingBudget,status:a.status})}catch(n){this.logger.error("Auto-rebid failed",{auctionId:t,error:n}),this.events.emit("auction:auto-rebid-failed",{auctionId:t,error:n})}}startMonitoring(t,i,e){this.monitorRuntime.start(t,i,e),this.trackedParticipations.has(t)||this.trackedParticipations.set(t,{auctionId:t,enteredAt:/* @__PURE__ */(new Date).toISOString(),status:"watching"}),this.startWatching(t)}stopMonitoring(t){this.stopWatching(t),this.monitorRuntime.stop(t)}isMonitoring(t){return this.monitorRuntime.has(t)||this.pollingIntervals.has(t)}destroy(){this.destroyed=!0,this.lifecycleGeneration+=1,this.stopAllWatching(),this.inFlightRequests.clear(),this.lastStatusCache.clear(),this.diagnosticEmissionIds.clear(),this.trackedParticipations.clear(),this.monitorGenerations.clear(),this.operationGenerations.clear(),this.bidHistoryCache.clear(),this.auctionDetailsCache.clear(),this.autoRebidConfigs.clear(),this.monitorRuntime.clear()}}export{m as AuctionManagementModule};
1
+ import{AuctionBidderStateResponseSchema as t,AuctionHighestBidResponseSchema as i,AuctionBidHistoryResponseSchema as e}from"@fanfare-io/fanfare-sdk-contracts/auction";import{createError as n}from"../core/errors.js";import{getLogger as a}from"../core/logger.js";import{isMoneyString as s,addMoney as o,isMoneyGreaterThan as r,normalizeMoney as d,isMoneyLessThanOrEqualTo as u,subtractMoney as c}from"../core/money.js";import{parseResponse as h}from"../core/parse-response.js";import{DistributionMonitorRuntime as l}from"../experiences/distribution-monitor.runtime.js";import{getEventBus as g}from"../state/events.js";import{getSDKStore as p}from"../state/store.js";class m{constructor(t){this.logger=a(),this.events=g(),this.trackedParticipations=/* @__PURE__ */new Map,this.inFlightRequests=/* @__PURE__ */new Map,this.pollingIntervals=/* @__PURE__ */new Map,this.lastStatusCache=/* @__PURE__ */new Map,this.autoRebidConfigs=/* @__PURE__ */new Map,this.monitorRuntime=new l,this.bidHistoryCache=/* @__PURE__ */new Map,this.auctionDetailsCache=/* @__PURE__ */new Map,this.monitorGenerations=/* @__PURE__ */new Map,this.operationGenerations=/* @__PURE__ */new Map,this.diagnosticEmissionIds=/* @__PURE__ */new Set,this.BID_HISTORY_CACHE_TTL=5e3,this.AUCTION_DETAILS_CACHE_TTL=3e4,this.destroyed=!1,this.lifecycleGeneration=0,this.http=t}get store(){return p()}computeMinNextBid(t,i){if(i&&s(t)&&s(i))return o(t,i)}async getAuctionDetailsCached(t){const i=this.auctionDetailsCache.get(t);if(i&&Date.now()-i.fetchedAt<this.AUCTION_DETAILS_CACHE_TTL)return i.details;const e=await this.http.get(`/auctions/${t}`);return this.auctionDetailsCache.set(t,{details:e,fetchedAt:Date.now()}),e}getTrackedParticipation(t){return this.trackedParticipations.get(t)}getAuctionCloseAt(t){return t.closeAt??t.settleAt}buildDisplayState(t,{status:i,highestBid:e,currentBid:n,bidCount:a}){return{highestBid:e,currentBid:n,bidCount:a,closeAt:this.getAuctionCloseAt(t),status:i,currencyCode:t.currencyCode,minNextBid:e?this.computeMinNextBid(e,t.minBidIncrement):void 0,bidIncrement:t.minBidIncrement??void 0}}emitTransitionEvents(t){for(const i of t)this.events.emit(i.type,i.payload)}applyTransition(t,i){i.trackedParticipation?this.trackedParticipations.set(t,i.trackedParticipation):this.trackedParticipations.delete(t);const e=this.monitorRuntime.getDisplayAtom(t);e&&i.displayState&&e.set({type:"auction",...i.displayState}),this.emitTransitionEvents(i.events),i.diagnosticError&&!this.diagnosticEmissionIds.has(t)?(this.diagnosticEmissionIds.add(t),this.logger.error("Auction monitoring diagnostic",{auctionId:t,error:i.diagnosticError}),this.events.emit("auction:error",{auctionId:t,error:i.diagnosticError})):i.diagnosticError||this.diagnosticEmissionIds.delete(t),i.monitorUpdate&&this.monitorRuntime.notify(t,i.monitorUpdate),i.stopWatching&&this.stopWatching(t),i.autoRebidStatus&&this.handleAutoRebid(t,i.autoRebidStatus)}checkOutbidConditions(t,i,e){if("winning"===t.status&&"outbid"===i.status)return!0;if(e?.currentBid&&i.highestBid){const n=e.currentBid,a=i.highestBid,o=t.highestBid||"0";if(s(n)&&s(a)&&s(o))return r(a,o)&&r(a,n)&&"winning"!==i.status}return"outbid"!==t.status&&"outbid"===i.status}buildBidTransition(t,i,e,n,a){const s="winning"===i.status?"winning":"outbid"===i.status?"outbid":"bidding";return{trackedParticipation:{auctionId:t,enteredAt:a?.enteredAt??/* @__PURE__ */(new Date).toISOString(),status:s,currentBid:e,highestBid:i.highestBid,bidCount:i.bidCount,lastBidAt:/* @__PURE__ */(new Date).toISOString(),metadata:a?.metadata,fingerprint:a?.fingerprint},displayState:this.buildDisplayState(n,{status:i.status,highestBid:i.highestBid,currentBid:e,bidCount:i.bidCount}),events:[{type:"auction:bid-placed",payload:{auctionId:t,amount:e,status:i.status}},..."winning"===i.status?[{type:"auction:winning",payload:{auctionId:t,amount:e,highestBid:i.highestBid}}]:[],..."outbid"===i.status?[{type:"auction:outbid",payload:{auctionId:t,yourBid:e,highestBid:i.highestBid}}]:[]],monitorUpdate:null,stopWatching:!1,autoRebidStatus:null}}buildStatusTransition(t,i,e,{previousParticipation:n,previousStatus:a}){const s=i.currentBid??n?.currentBid,o=i.bidCount??n?.bidCount;if(i.timeRemaining<=0){const a="winning"===i.status;return a&&!i.admissionGrant?{trackedParticipation:{auctionId:t,enteredAt:n?.enteredAt??/* @__PURE__ */(new Date).toISOString(),status:"winning",currentBid:s,highestBid:i.highestBid,bidCount:o,metadata:n?.metadata,fingerprint:n?.fingerprint},displayState:this.buildDisplayState(e,{status:"winning",highestBid:i.highestBid,currentBid:s,bidCount:o}),events:[],monitorUpdate:null,stopWatching:!1,autoRebidStatus:null,diagnosticError:new Error("Auction resolved to terminal winning without admissionGrant")}:{trackedParticipation:null,displayState:this.buildDisplayState(e,{status:a?"won":"lost",highestBid:i.highestBid,currentBid:s,bidCount:o}),events:a?[{type:"auction:won",payload:{auctionId:t,winningBid:s??""}}]:[{type:"auction:lost",payload:{auctionId:t,highestBid:i.highestBid}}],monitorUpdate:a&&i.admissionGrant?{type:"granted",token:i.admissionGrant}:a?null:{type:"ended",outcome:{type:"lost"}},stopWatching:!0,autoRebidStatus:null}}const r={auctionId:t,enteredAt:n?.enteredAt??/* @__PURE__ */(new Date).toISOString(),status:i.status,currentBid:s,highestBid:i.highestBid,bidCount:o,metadata:n?.metadata,fingerprint:n?.fingerprint},d=[];let u=null;return a&&(this.checkOutbidConditions(a,i,n)&&(d.push({type:"auction:outbid",payload:{auctionId:t,yourBid:a.currentBid||n?.currentBid||"",highestBid:i.highestBid}}),u=i),"winning"!==a.status&&"winning"===i.status&&d.push({type:"auction:winning",payload:{auctionId:t,amount:s||"",highestBid:i.highestBid}}),a.highestBid!==i.highestBid&&"winning"!==i.status&&d.push({type:"auction:bid-updated",payload:{auctionId:t,highestBid:i.highestBid,bidCount:i.bidCount||0}})),{trackedParticipation:r,displayState:this.buildDisplayState(e,{status:i.status,highestBid:i.highestBid,currentBid:s,bidCount:o}),events:d,monitorUpdate:null,stopWatching:!1,autoRebidStatus:u}}buildMissingTransition(){return{trackedParticipation:null,displayState:{status:"lost"},events:[],monitorUpdate:{type:"ended",outcome:{type:"closed",reason:"not_participating"}},stopWatching:!0,autoRebidStatus:null}}async get(t){try{return await this.getAuctionDetailsCached(t)}catch(i){throw this.logger.error("Failed to get auction",{auctionId:t,error:i}),i}}async bid(e,a,o){try{const r=this.lifecycleGeneration;if(!s(a))throw n.validationError("Invalid bid amount format. Must be a numeric string (e.g., '100.00')");if(!this.store.session)throw n.unauthorized("Not authenticated. Call auth.guest() or auth.login() first");let u=this.bumpOperationGeneration(e),c=this.getTrackedParticipation(e);c||(this.logger.warn("Attempting to bid without entering auction first",{auctionId:e}),await this.enter(e,void 0,o),c=this.getTrackedParticipation(e),u=this.bumpOperationGeneration(e)),this.logger.info("Placing bid",{auctionId:e,amount:a});const l=`/auctions/${e}/bid`,g=`/auctions/${e}/highest-bid`,[p,m,b]=await Promise.all([this.http.post(l,{amount:a},o),this.http.get(g),this.getAuctionDetailsCached(e)]),y=h(t,p,{endpoint:l},{throwOnFailure:!0});if(!y.ok)throw y.error;const I=h(i,m,{endpoint:g},{throwOnFailure:!0});if(!I.ok)throw I.error;const f=y.value,B=I.value,w=d(B.amount),v={status:"WINNING"===f.status||"WON"===f.status?"winning":"OUTBID"===f.status||"BIDDING"===f.status||"LOST"===f.status?"outbid":"accepted",amount:a,highestBid:w,bidCount:f.bidCount??(c?.bidCount||0)+1};return this.canApplyAsyncResult(e,r,void 0,u)?(this.applyTransition(e,this.buildBidTransition(e,v,a,b,c)),v):v}catch(r){throw this.logger.error("Failed to place bid",{auctionId:e,amount:a,error:r}),this.events.emit("auction:error",{auctionId:e,error:r}),r}}async enter(t,i,e){try{const a=this.lifecycleGeneration,s=this.store.session;if(!s)throw n.unauthorized("Not authenticated. Call auth.guest() or auth.login() first");const o=this.bumpOperationGeneration(t);if(this.logger.info("Entering auction",{auctionId:t}),await this.http.post(`/auctions/${t}/enter`,{metadata:i},e),!this.canApplyAsyncResult(t,a,void 0,o))return;this.trackedParticipations.set(t,{auctionId:t,enteredAt:/* @__PURE__ */(new Date).toISOString(),status:"watching",metadata:i,fingerprint:s.deviceFingerprint}),this.emitTransitionEvents([{type:"auction:entered",payload:{auctionId:t}}])}catch(a){throw this.logger.error("Failed to enter auction",{auctionId:t,error:a}),this.events.emit("auction:error",{auctionId:t,error:a}),a}}async leave(t,i){try{const n=this.lifecycleGeneration,a=this.bumpOperationGeneration(t);this.logger.info("Leaving auction",{auctionId:t});try{await this.http.post(`/auctions/${t}/leave`,void 0,i)}catch(e){if(404!==e?.status)throw e;this.logger.debug("Leave endpoint not available",{auctionId:t})}if(!this.canApplyAsyncResult(t,n,void 0,a))return;this.stopMonitoring(t),this.trackedParticipations.delete(t),this.emitTransitionEvents([{type:"auction:left",payload:{auctionId:t}}])}catch(e){throw this.logger.error("Failed to leave auction",{auctionId:t,error:e}),this.events.emit("auction:error",{auctionId:t,error:e}),e}}async status(t){const i=this.inFlightRequests.get(t);if(i)return this.logger.debug("Returning existing in-flight request",{auctionId:t}),i;const e=this.lifecycleGeneration,n=this.bumpOperationGeneration(t),a=this.doStatus(t,e,void 0,n).finally(()=>{this.inFlightRequests.delete(t)});return this.inFlightRequests.set(t,a),a}async doStatus(e,n,a,s){try{const o=`/auctions/${e}/status`,r=`/auctions/${e}/highest-bid`,[u,c,l]=await Promise.all([this.http.get(o),this.http.get(r),this.getAuctionDetailsCached(e)]),g=h(t,u,{endpoint:o},{throwOnFailure:!0});if(!g.ok)throw g.error;const p=h(i,c,{endpoint:r},{throwOnFailure:!0});if(!p.ok)throw p.error;const m=g.value,b=p.value,y=this.getAuctionCloseAt(l),I=y?new Date(y).getTime():Number.NaN,f=d(b.amount),B=this.getTrackedParticipation(e),w=m.lastBidAmount??m.winningBidAmount;let v="watching";"WINNING"===m.status||"WON"===m.status?v="winning":"OUTBID"!==m.status&&"LOST"!==m.status&&"BIDDING"!==m.status||(v="outbid");const A={auctionId:e,status:v,currentBid:B?.currentBid??(w?d(w):void 0),highestBid:f,timeRemaining:Number.isFinite(I)?Math.max(0,I-Date.now()):0,bidCount:m.bidCount,admissionGrant:m.admissionGrant};if(!this.canApplyAsyncResult(e,n,a,s))return A;const C=this.buildStatusTransition(e,A,l,{previousParticipation:B,previousStatus:this.lastStatusCache.get(e)});return this.applyTransition(e,C),C.stopWatching?this.lastStatusCache.delete(e):this.lastStatusCache.set(e,A),this.events.emit("auction:status-updated",{auctionId:e,status:A}),A}catch(o){if(o&&"object"==typeof o&&"status"in o&&404===o.status){const t={auctionId:e,status:"watching",highestBid:"0.00",timeRemaining:0};if(!this.canApplyAsyncResult(e,n,a,s))return t;const i=this.buildMissingTransition();return this.applyTransition(e,i),this.lastStatusCache.delete(e),this.events.emit("auction:status-updated",{auctionId:e,status:t}),t}if(!this.canApplyAsyncResult(e,n,a,s))return this.lastStatusCache.get(e)??{auctionId:e,status:"watching",highestBid:"0.00",timeRemaining:0};throw this.logger.error("Failed to get auction status",{auctionId:e,error:o}),this.events.emit("auction:error",{auctionId:e,error:o}),o}}async getBidHistory(t){try{const i=this.bidHistoryCache.get(t);if(i&&Date.now()-i.fetchedAt<this.BID_HISTORY_CACHE_TTL)return this.logger.debug("Returning cached bid history",{auctionId:t}),i.bids;this.logger.info("Fetching bid history",{auctionId:t});const n=`/auctions/${t}/bids/history`,a=await this.http.get(n),s=h(e,a,{endpoint:n},{throwOnFailure:!0});if(!s.ok)throw s.error;const o=s.value.map(t=>({amount: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){this.stopWatching(t);const e=this.bumpMonitorGeneration(t);this.logger.info("Starting auction status polling",{auctionId:t,intervalMs:i});const n=this.getOperationGeneration(t);this.doStatus(t,this.lifecycleGeneration,e,n).catch(i=>{this.canApplyAsyncResult(t,this.lifecycleGeneration,e,n)&&this.logger.error("Initial status check failed",{auctionId:t,error:i})});const a=setInterval(()=>{const i=this.getOperationGeneration(t);this.doStatus(t,this.lifecycleGeneration,e,i).catch(n=>{this.canApplyAsyncResult(t,this.lifecycleGeneration,e,i)&&this.logger.error("Polling failed",{auctionId:t,error:n})})},i);this.pollingIntervals.set(t,a),this.events.emit("auction:watching-started",{auctionId:t,intervalMs:i})}stopWatching(t){this.bumpMonitorGeneration(t);const i=this.pollingIntervals.get(t);i&&(clearInterval(i),this.pollingIntervals.delete(t),this.lastStatusCache.delete(t),this.logger.info("Stopped watching auction",{auctionId:t}),this.events.emit("auction:watching-stopped",{auctionId:t}))}stopAllWatching(){for(const t of Array.from(this.pollingIntervals.keys()))this.stopWatching(t)}bumpMonitorGeneration(t){const i=(this.monitorGenerations.get(t)??0)+1;return this.monitorGenerations.set(t,i),i}getOperationGeneration(t){return this.operationGenerations.get(t)??0}bumpOperationGeneration(t){const i=this.getOperationGeneration(t)+1;return this.operationGenerations.set(t,i),i}canApplyAsyncResult(t,i,e,n){return!this.destroyed&&this.lifecycleGeneration===i&&((void 0===e||this.monitorGenerations.get(t)===e)&&(void 0===n||this.getOperationGeneration(t)===n))}enableAutoRebid(t,i,e){if(!s(i)||!s(e))throw n.validationError("Invalid monetary format. Must be numeric strings (e.g., '100.00')");if(u(i,"0")||u(e,"0"))throw n.validationError("Max bid and increment must be positive");if(r(e,i))throw n.validationError("Increment cannot be greater than max bid");const a=this.getTrackedParticipation(t);if(!a)throw n.validationError("Must enter auction before enabling auto-rebid");const o=d(a.currentBid),h=c(i,o);this.autoRebidConfigs.set(t,{enabled:!0,maxBid:i,increment:e,remainingBudget:h,rebidCount:0}),this.logger.info("Auto-rebid enabled",{auctionId:t,maxBid:i,increment:e,remainingBudget:h}),this.events.emit("auction:auto-rebid-enabled",{auctionId:t,maxBid:i,increment:e})}disableAutoRebid(t){const i=this.autoRebidConfigs.get(t);i&&(i.enabled=!1,this.logger.info("Auto-rebid disabled",{auctionId:t}),this.events.emit("auction:auto-rebid-disabled",{auctionId:t,finalRebidCount:i.rebidCount||0}))}getAutoRebidConfig(t){return this.autoRebidConfigs.get(t)}async handleAutoRebid(t,i){const e=this.autoRebidConfigs.get(t);if(e&&e.enabled)try{if(!s(i.highestBid)||!s(e.maxBid)||!s(e.increment))return void this.logger.error("Invalid auto-rebid amounts",{auctionId:t,config:e});const n=o(i.highestBid,e.increment);if(r(n,e.maxBid))return this.logger.info("Auto-rebid would exceed max bid, disabling",{auctionId:t,wouldBid:n,maxBid:e.maxBid}),this.disableAutoRebid(t),void this.events.emit("auction:auto-rebid-max-reached",{auctionId:t,maxBid:e.maxBid,wouldNeedToBid:n});this.logger.info("Placing auto-rebid",{auctionId:t,amount:n});const a=await this.bid(t,n);e.lastRebidAt=/* @__PURE__ */(new Date).toISOString(),e.rebidCount=(e.rebidCount||0)+1,e.remainingBudget=c(e.maxBid,n),this.events.emit("auction:auto-rebid-placed",{auctionId:t,amount:n,remainingBudget:e.remainingBudget,status:a.status})}catch(n){this.logger.error("Auto-rebid failed",{auctionId:t,error:n}),this.events.emit("auction:auto-rebid-failed",{auctionId:t,error:n})}}startMonitoring(t,i,e){this.monitorRuntime.start(t,i,e),this.trackedParticipations.has(t)||this.trackedParticipations.set(t,{auctionId:t,enteredAt:/* @__PURE__ */(new Date).toISOString(),status:"watching"}),this.startWatching(t)}stopMonitoring(t){this.stopWatching(t),this.monitorRuntime.stop(t)}isMonitoring(t){return this.monitorRuntime.has(t)||this.pollingIntervals.has(t)}destroy(){this.destroyed=!0,this.lifecycleGeneration+=1,this.stopAllWatching(),this.inFlightRequests.clear(),this.lastStatusCache.clear(),this.diagnosticEmissionIds.clear(),this.trackedParticipations.clear(),this.monitorGenerations.clear(),this.operationGenerations.clear(),this.bidHistoryCache.clear(),this.auctionDetailsCache.clear(),this.autoRebidConfigs.clear(),this.monitorRuntime.clear()}}export{m as AuctionManagementModule};